mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
2701-есть адекватный порядок для строк черновика и фикс пустого поиска
This commit is contained in:
@@ -155,6 +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/commit", draftsHandler.CommitDraft)
|
api.POST("/drafts/:id/commit", draftsHandler.CommitDraft)
|
||||||
api.POST("/drafts/container", draftsHandler.AddContainer)
|
api.POST("/drafts/container", draftsHandler.AddContainer)
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ type DraftInvoiceItem struct {
|
|||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
DraftID uuid.UUID `gorm:"type:uuid;not null;index" json:"draft_id"`
|
DraftID uuid.UUID `gorm:"type:uuid;not null;index" json:"draft_id"`
|
||||||
|
|
||||||
|
// Порядок отображения позиции в черновике
|
||||||
|
Order int `gorm:"not null;default:0" json:"order"`
|
||||||
|
|
||||||
RawName string `gorm:"type:varchar(255);not null" json:"raw_name"`
|
RawName string `gorm:"type:varchar(255);not null" json:"raw_name"`
|
||||||
RawAmount decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_amount"`
|
RawAmount decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_amount"`
|
||||||
RawPrice decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_price"`
|
RawPrice decimal.Decimal `gorm:"type:numeric(19,4)" json:"raw_price"`
|
||||||
@@ -87,6 +90,7 @@ type Repository interface {
|
|||||||
CreateItems(items []DraftInvoiceItem) error
|
CreateItems(items []DraftInvoiceItem) error
|
||||||
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
|
||||||
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
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (r *pgRepository) GetByID(id uuid.UUID) (*drafts.DraftInvoice, error) {
|
|||||||
var draft drafts.DraftInvoice
|
var draft drafts.DraftInvoice
|
||||||
err := r.db.
|
err := r.db.
|
||||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Order("draft_invoice_items.raw_name ASC")
|
return db.Order("draft_invoice_items.order ASC")
|
||||||
}).
|
}).
|
||||||
Preload("Items.Product").
|
Preload("Items.Product").
|
||||||
Preload("Items.Product.MainUnit").
|
Preload("Items.Product.MainUnit").
|
||||||
@@ -98,6 +98,13 @@ func (r *pgRepository) UpdateItem(itemID uuid.UUID, updates map[string]interface
|
|||||||
Updates(updates).Error
|
Updates(updates).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateItemOrder обновляет порядок позиции
|
||||||
|
func (r *pgRepository) UpdateItemOrder(itemID uuid.UUID, newOrder int) error {
|
||||||
|
return r.db.Model(&drafts.DraftInvoiceItem{}).
|
||||||
|
Where("id = ?", itemID).
|
||||||
|
Update("order", newOrder).Error
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,6 +169,20 @@ func (s *Service) UpdateDraftHeader(id uuid.UUID, storeID *uuid.UUID, supplierID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
|
func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
|
||||||
|
// Получаем текущий черновик для определения максимального order
|
||||||
|
draft, err := s.draftRepo.GetByID(draftID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим максимальный order среди существующих позиций
|
||||||
|
maxOrder := 0
|
||||||
|
for _, item := range draft.Items {
|
||||||
|
if item.Order > maxOrder {
|
||||||
|
maxOrder = item.Order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newItem := &drafts.DraftInvoiceItem{
|
newItem := &drafts.DraftInvoiceItem{
|
||||||
ID: uuid.New(),
|
ID: uuid.New(),
|
||||||
DraftID: draftID,
|
DraftID: draftID,
|
||||||
@@ -181,6 +195,7 @@ func (s *Service) AddItem(draftID uuid.UUID) (*drafts.DraftInvoiceItem, error) {
|
|||||||
IsMatched: false,
|
IsMatched: false,
|
||||||
LastEditedField1: drafts.FieldQuantity,
|
LastEditedField1: drafts.FieldQuantity,
|
||||||
LastEditedField2: drafts.FieldPrice,
|
LastEditedField2: drafts.FieldPrice,
|
||||||
|
Order: maxOrder + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.draftRepo.CreateItem(newItem); err != nil {
|
if err := s.draftRepo.CreateItem(newItem); err != nil {
|
||||||
@@ -854,3 +869,55 @@ func (s *Service) SetDraftReadyToVerify(draftID, userID uuid.UUID) (*drafts.Draf
|
|||||||
|
|
||||||
return draft, nil
|
return draft, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReorderItem изменяет порядок позиции в черновике
|
||||||
|
func (s *Service) ReorderItem(draftID, itemID uuid.UUID, newOrder int) error {
|
||||||
|
// Получаем черновик
|
||||||
|
draft, err := s.draftRepo.GetByID(draftID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим перемещаемый элемент
|
||||||
|
var targetItem *drafts.DraftInvoiceItem
|
||||||
|
for _, item := range draft.Items {
|
||||||
|
if item.ID == itemID {
|
||||||
|
targetItem = &item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetItem == nil {
|
||||||
|
return fmt.Errorf("item not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
oldOrder := targetItem.Order
|
||||||
|
|
||||||
|
// Если порядок не изменился, ничего не делаем
|
||||||
|
if oldOrder == newOrder {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем порядок других элементов
|
||||||
|
if newOrder < oldOrder {
|
||||||
|
// Перемещаем вверх: увеличиваем order элементов между newOrder и oldOrder-1
|
||||||
|
for _, item := range draft.Items {
|
||||||
|
if item.Order >= newOrder && item.Order < oldOrder && item.ID != itemID {
|
||||||
|
if err := s.draftRepo.UpdateItemOrder(item.ID, item.Order+1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Перемещаем вниз: уменьшаем order элементов между oldOrder+1 и newOrder
|
||||||
|
for _, item := range draft.Items {
|
||||||
|
if item.Order > oldOrder && item.Order <= newOrder && item.ID != itemID {
|
||||||
|
if err := s.draftRepo.UpdateItemOrder(item.ID, item.Order-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем порядок целевого элемента
|
||||||
|
return s.draftRepo.UpdateItemOrder(itemID, newOrder)
|
||||||
|
}
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ func (s *Service) ProcessDocument(ctx context.Context, userID uuid.UUID, imgData
|
|||||||
|
|
||||||
// 7. Матчинг и сохранение позиций
|
// 7. Матчинг и сохранение позиций
|
||||||
var draftItems []drafts.DraftInvoiceItem
|
var draftItems []drafts.DraftInvoiceItem
|
||||||
for _, rawItem := range rawResult.Items {
|
for i, rawItem := range rawResult.Items {
|
||||||
item := drafts.DraftInvoiceItem{
|
item := drafts.DraftInvoiceItem{
|
||||||
DraftID: draft.ID,
|
DraftID: draft.ID,
|
||||||
RawName: rawItem.RawName,
|
RawName: rawItem.RawName,
|
||||||
@@ -170,6 +170,7 @@ func (s *Service) ProcessDocument(ctx context.Context, userID uuid.UUID, imgData
|
|||||||
Quantity: decimal.NewFromFloat(rawItem.Amount),
|
Quantity: decimal.NewFromFloat(rawItem.Amount),
|
||||||
Price: decimal.NewFromFloat(rawItem.Price),
|
Price: decimal.NewFromFloat(rawItem.Price),
|
||||||
Sum: decimal.NewFromFloat(rawItem.Sum),
|
Sum: decimal.NewFromFloat(rawItem.Sum),
|
||||||
|
Order: i + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
match, _ := s.ocrRepo.FindMatch(serverID, rawItem.RawName)
|
match, _ := s.ocrRepo.FindMatch(serverID, rawItem.RawName)
|
||||||
|
|||||||
@@ -302,3 +302,31 @@ 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 изменяет порядок позиции в черновике
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.ReorderItem(draftID, req.ItemID, req.NewOrder); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- Добавляем колонку order в таблицу draft_invoice_items
|
||||||
|
-- Миграция для добавления поля сортировки позиций в черновике накладной
|
||||||
|
|
||||||
|
-- Добавляем колонку order со значением по умолчанию 0
|
||||||
|
ALTER TABLE draft_invoice_items ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Инициализируем значения order для существующих записей
|
||||||
|
-- Сортируем по created_at для сохранения текущего порядка
|
||||||
|
WITH ordered_items AS (
|
||||||
|
SELECT id, ROW_NUMBER() OVER (PARTITION BY draft_id ORDER BY created_at ASC) as row_num
|
||||||
|
FROM draft_invoice_items
|
||||||
|
)
|
||||||
|
UPDATE draft_invoice_items
|
||||||
|
SET "order" = ordered_items.row_num
|
||||||
|
FROM ordered_items
|
||||||
|
WHERE draft_invoice_items.id = ordered_items.id;
|
||||||
|
|
||||||
|
-- Создаем индекс для оптимизации сортировки
|
||||||
|
CREATE INDEX idx_draft_items_order ON draft_invoice_items(draft_id, "order");
|
||||||
@@ -25,6 +25,7 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [options, setOptions] = useState<SelectOption[]>([]);
|
const [options, setOptions] = useState<SelectOption[]>([]);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
const fetchRef = useRef<number | null>(null);
|
const fetchRef = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
const fetchProducts = async (search: string) => {
|
const fetchProducts = async (search: string) => {
|
||||||
if (!search) {
|
if (!search) {
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
setNotFound(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setFetching(true);
|
setFetching(true);
|
||||||
@@ -57,8 +59,11 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
data: item,
|
data: item,
|
||||||
}));
|
}));
|
||||||
setOptions(newOptions);
|
setOptions(newOptions);
|
||||||
|
// Показываем "Не найдено" если результатов нет
|
||||||
|
setNotFound(results.length === 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
setNotFound(true);
|
||||||
} finally {
|
} finally {
|
||||||
setFetching(false);
|
setFetching(false);
|
||||||
}
|
}
|
||||||
@@ -68,6 +73,8 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
if (fetchRef.current !== null) {
|
if (fetchRef.current !== null) {
|
||||||
window.clearTimeout(fetchRef.current);
|
window.clearTimeout(fetchRef.current);
|
||||||
}
|
}
|
||||||
|
// Сбрасываем notFound при новом поиске
|
||||||
|
setNotFound(false);
|
||||||
// Запускаем поиск только если введено хотя бы 2 символа
|
// Запускаем поиск только если введено хотя бы 2 символа
|
||||||
if (val.length < 2) {
|
if (val.length < 2) {
|
||||||
return;
|
return;
|
||||||
@@ -93,7 +100,13 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
placeholder="Начните вводить название товара..."
|
placeholder="Начните вводить название товара..."
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
notFoundContent={fetching ? <Spin size="small" /> : null}
|
notFoundContent={
|
||||||
|
fetching ? (
|
||||||
|
<Spin size="small" />
|
||||||
|
) : notFound ? (
|
||||||
|
<div style={{ padding: "8px", color: "#999" }}>Товар не найден</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -101,8 +114,11 @@ export const CatalogSelect: React.FC<Props> = ({
|
|||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
listHeight={256}
|
listHeight={256}
|
||||||
allowClear
|
allowClear
|
||||||
// При очистке сбрасываем опции, чтобы при следующем клике не вылезал старый товар
|
// При очистке сбрасываем опции и notFound, чтобы при следующем клике не вылезал старый товар
|
||||||
onClear={() => setOptions([])}
|
onClear={() => {
|
||||||
|
setOptions([]);
|
||||||
|
setNotFound(false);
|
||||||
|
}}
|
||||||
// При клике (фокусе), если поле пустое - можно показать дефолтные опции или оставить пустым
|
// При клике (фокусе), если поле пустое - можно показать дефолтные опции или оставить пустым
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
if (!value) setOptions([]);
|
if (!value) setOptions([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user