From a536b3ff3c07e4b3df6c03c86ba31f3b96fdb3c4 Mon Sep 17 00:00:00 2001 From: SERTY Date: Wed, 28 Jan 2026 03:58:43 +0300 Subject: [PATCH] =?UTF-8?q?2801-=D0=BE=D0=BF=D1=86=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D1=82=D0=B0=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B2=20=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BA=D0=B5.=20=D0=BF=D0=BE=D1=84=D0=B8=D0=BA=D1=81=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B8=D0=BD=D0=BA=20=D0=BD=D0=B0=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=BD=D1=8B=D1=85=20=D1=81=D0=B2=D0=B0=D0=B9=D0=BF?= =?UTF-8?q?=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B2=D0=BD=D0=B5=D1=88?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/infrastructure/rms/client.go | 9 +- internal/services/drafts/service.go | 8 + internal/services/sync/service.go | 2 +- internal/transport/http/handlers/drafts.go | 2 +- rmser-view/project_context.md | 501 ++++++++++-------- .../src/components/invoices/DraftItemRow.tsx | 46 +- rmser-view/src/pages/DraftsList.tsx | 34 +- rmser-view/src/pages/InvoiceDraftPage.tsx | 15 +- rmser-view/src/pages/SettingsPage.tsx | 39 ++ rmser-view/src/services/api.ts | 5 + 10 files changed, 374 insertions(+), 287 deletions(-) diff --git a/internal/infrastructure/rms/client.go b/internal/infrastructure/rms/client.go index 54b1ae5..e91c0bc 100644 --- a/internal/infrastructure/rms/client.go +++ b/internal/infrastructure/rms/client.go @@ -573,7 +573,7 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) { reqDTO := IncomingInvoiceImportXML{ DocumentNumber: inv.DocumentNumber, - IncomingDocumentNumber: inv.IncomingDocumentNumber, // Присваиваем входящий номер документа из домена + IncomingDocumentNumber: inv.IncomingDocumentNumber, DateIncoming: inv.DateIncoming.Format("02.01.2006"), DefaultStore: inv.DefaultStoreID.String(), Supplier: inv.SupplierID.String(), @@ -581,6 +581,13 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) { Comment: comment, } + logger.Log.Info("RMS Invoice Import Debug", + zap.String("document_number", inv.DocumentNumber), + zap.String("incoming_document_number", inv.IncomingDocumentNumber), + zap.String("supplier_id", inv.SupplierID.String()), + zap.String("store_id", inv.DefaultStoreID.String()), + ) + if inv.ID != uuid.Nil { reqDTO.ID = inv.ID.String() } diff --git a/internal/services/drafts/service.go b/internal/services/drafts/service.go index 4009be4..45eb49d 100644 --- a/internal/services/drafts/service.go +++ b/internal/services/drafts/service.go @@ -422,6 +422,14 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) { if err != nil { logger.Log.Warn("Не удалось получить список накладных для поиска UUID", zap.Error(err), zap.Time("date", *draft.DateIncoming)) } else { + // ВАЖНО: Сохраняем полученные накладные, чтобы они сразу появились в базе как SYNCED + for i := range invoices { + invoices[i].RMSServerID = server.ID + } + if err := s.invoiceRepo.SaveInvoices(invoices); err != nil { + logger.Log.Error("Failed to save committed invoices", zap.Error(err)) + } + found := false for _, invoice := range invoices { if invoice.DocumentNumber == docNum { diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index 8bf685e..da3ae68 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -211,7 +211,7 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) er return err } if lastDate != nil { - from = *lastDate + from = lastDate.AddDate(0, 0, -7) } else { from = time.Now().AddDate(0, 0, -45) } diff --git a/internal/transport/http/handlers/drafts.go b/internal/transport/http/handlers/drafts.go index a90ed15..b27d46a 100644 --- a/internal/transport/http/handlers/drafts.go +++ b/internal/transport/http/handlers/drafts.go @@ -195,7 +195,7 @@ type CommitRequestDTO struct { StoreID string `json:"store_id"` SupplierID string `json:"supplier_id"` Comment string `json:"comment"` - IncomingDocNum string `json:"incoming_doc_num"` + IncomingDocNum string `json:"incoming_document_number"` } func (h *DraftsHandler) CommitDraft(c *gin.Context) { diff --git a/rmser-view/project_context.md b/rmser-view/project_context.md index 7e19b02..666512a 100644 --- a/rmser-view/project_context.md +++ b/rmser-view/project_context.md @@ -1,6 +1,6 @@ # =================================================================== # Полный контекст React Typescript проекта -# Сгенерировано: 2026-01-27 11:40:29 +# Сгенерировано: 2026-01-28 02:48:31 # =================================================================== Это полный дамп исходного кода React Typescript (Vite) проекта. @@ -5106,6 +5106,7 @@ interface Props { onDelete: (itemId: string) => void; isUpdating: boolean; recommendations?: Recommendation[]; + isReordering: boolean; } type FieldType = "quantity" | "price" | "sum"; @@ -5117,6 +5118,7 @@ export const DraftItemRow: React.FC = ({ onDelete, isUpdating, recommendations = [], + isReordering, }) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -5359,224 +5361,228 @@ export const DraftItemRow: React.FC = ({ return ( <> - - {(provided, snapshot) => ( -
- + {(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 ( +
- {/* Drag handle - иконка для перетаскивания */} -
{ - e.currentTarget.style.color = "#1890ff"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.color = "#8c8c8c"; + 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 || "Новая позиция"} - - {item.raw_amount > 0 && ( - - (чек: {item.raw_amount} x {item.raw_price}) - - )} -
+ {/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */} + {isReordering && (
{ + e.currentTarget.style.color = "#1890ff"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = "#8c8c8c"; }} > - {isUpdating && ( - - )} - - {activeWarning && ( - - )} - - {!item.product_id && ( - - ? - - )} - - onDelete(item.id)} - okText="Да" - cancelText="Нет" - placement="left" - > -
-
- - - - {activeProduct && ( - ( + <> + {menu} + + + + )} + /> + )} +
- handleValueChange("quantity", val)} - onBlur={() => handleBlur("quantity")} - precision={3} - /> - x - handleValueChange("price", val)} - onBlur={() => handleBlur("price")} - precision={2} - /> -
+
+ handleValueChange("quantity", val)} + onBlur={() => handleBlur("quantity")} + precision={3} + /> + x + handleValueChange("price", val)} + onBlur={() => handleBlur("price")} + precision={2} + /> +
-
- = - handleValueChange("sum", val)} - onBlur={() => handleBlur("sum")} - precision={2} - /> +
+ = + handleValueChange("sum", val)} + onBlur={() => handleBlur("sum")} + precision={2} + /> +
-
- -
-
- )} + + + + ); + }}
{activeProduct && ( { ``` // src/pages/DraftsList.tsx -import React, { useState, useMemo, useCallback, useRef } from "react"; +import React, { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { List, @@ -7209,9 +7215,6 @@ export const DraftsList: React.FC = () => { ); const [endDate, setEndDate] = useState(dayjs()); - const touchStartX = useRef(0); - const touchEndX = useRef(0); - const { data: invoices, isLoading, @@ -7316,14 +7319,6 @@ export const DraftsList: React.FC = () => { setCurrentPage(1); }; - const handleTouchStart = useCallback((e: React.TouchEvent) => { - touchStartX.current = e.changedTouches[0].screenX; - }, []); - - const handleTouchMove = useCallback((e: React.TouchEvent) => { - touchEndX.current = e.changedTouches[0].screenX; - }, []); - const getItemDate = (item: UnifiedInvoice) => item.type === "DRAFT" ? item.created_at : item.date_incoming; @@ -7361,21 +7356,6 @@ export const DraftsList: React.FC = () => { return result; }, [invoices, filterType]); - const handleTouchEnd = useCallback(() => { - const diff = touchStartX.current - touchEndX.current; - const totalPages = Math.ceil( - (filteredAndSortedInvoices.length || 0) / pageSize - ); - - if (Math.abs(diff) > 50) { - if (diff > 0 && currentPage < totalPages) { - setCurrentPage((prev) => prev + 1); - } else if (diff < 0 && currentPage > 1) { - setCurrentPage((prev) => prev - 1); - } - } - }, [currentPage, pageSize, filteredAndSortedInvoices]); - const paginatedInvoices = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize); @@ -7482,11 +7462,7 @@ export const DraftsList: React.FC = () => { ) : ( <> -
+
{Object.entries(groupedInvoices).map(([dateKey, items]) => (
@@ -7707,6 +7683,7 @@ import { RestOutlined, PlusOutlined, FileImageOutlined, + SwapOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; import { api, getStaticUrl } from "../services/api"; @@ -7730,6 +7707,8 @@ export const InvoiceDraftPage: React.FC = () => { const [updatingItems, setUpdatingItems] = useState>(new Set()); const [itemsOrder, setItemsOrder] = useState>({}); + const [isDragging, setIsDragging] = useState(false); + const [isReordering, setIsReordering] = useState(false); // Состояние для просмотра фото чека const [previewVisible, setPreviewVisible] = useState(false); @@ -7752,6 +7731,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; }, @@ -7849,6 +7829,7 @@ export const InvoiceDraftPage: React.FC = () => { }); // --- ЭФФЕКТЫ --- + useEffect(() => { if (draft) { const currentValues = form.getFieldsValue(); @@ -7939,7 +7920,12 @@ export const InvoiceDraftPage: React.FC = () => { }); }; + const handleDragStart = () => { + setIsDragging(true); + }; + const handleDragEnd = async (result: DropResult) => { + setIsDragging(false); const { source, destination } = result; // Если нет назначения или позиция не изменилась @@ -8059,7 +8045,7 @@ export const InvoiceDraftPage: React.FC = () => {
- {/* Правая часть хедера: Кнопка чека и Кнопка удаления */} + {/* Правая часть хедера: Кнопка чека, Кнопка перетаскивания и Кнопка удаления */}
{/* Кнопка просмотра чека (только если есть URL) */} {draft.photo_url && ( @@ -8072,6 +8058,16 @@ export const InvoiceDraftPage: React.FC = () => { )} + {/* Кнопка переключения режима перетаскивания */} + +
{/* Items List */} - + {(provided, snapshot) => (
{ onDelete={(itemId) => deleteItemMutation.mutate(itemId)} isUpdating={updatingItems.has(item.id)} recommendations={recommendationsQuery.data || []} + isReordering={isReordering} /> ))} {provided.placeholder} @@ -8700,6 +8697,7 @@ import { Spin, message, Tabs, + Popconfirm, } from "antd"; import { SaveOutlined, @@ -8758,6 +8756,17 @@ export const SettingsPage: React.FC = () => { }, }); + const deleteAllDraftsMutation = useMutation({ + mutationFn: () => api.deleteAllDrafts(), + onSuccess: (data) => { + message.success(`Удалено черновиков: ${data.count}`); + queryClient.invalidateQueries({ queryKey: ["stats"] }); + }, + onError: () => { + message.error("Не удалось удалить черновики"); + }, + }); + // --- Эффекты --- useEffect(() => { @@ -8871,6 +8880,33 @@ export const SettingsPage: React.FC = () => { > Сохранить настройки + + {currentUserRole === "OWNER" && ( + + + Опасная зона + + deleteAllDraftsMutation.mutate()} + okText="Удалить" + cancelText="Отмена" + okButtonProps={{ danger: true }} + > + + + + )} ); @@ -9193,6 +9229,11 @@ export const api = { deleteDraft: async (id: string): Promise => { await apiClient.delete(`/drafts/${id}`); }, + + deleteAllDrafts: async (): Promise<{ count: number }> => { + const { data } = await apiClient.delete<{ count: number }>('/drafts'); + return data; + }, // --- Настройки и Статистика --- diff --git a/rmser-view/src/components/invoices/DraftItemRow.tsx b/rmser-view/src/components/invoices/DraftItemRow.tsx index f9d6cb6..61ca948 100644 --- a/rmser-view/src/components/invoices/DraftItemRow.tsx +++ b/rmser-view/src/components/invoices/DraftItemRow.tsx @@ -38,6 +38,7 @@ interface Props { onDelete: (itemId: string) => void; isUpdating: boolean; recommendations?: Recommendation[]; + isReordering: boolean; } type FieldType = "quantity" | "price" | "sum"; @@ -49,6 +50,7 @@ export const DraftItemRow: React.FC = ({ onDelete, isUpdating, recommendations = [], + isReordering, }) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -291,7 +293,7 @@ export const DraftItemRow: React.FC = ({ return ( <> - + {(provided, snapshot) => { const style = { marginBottom: "8px", @@ -325,26 +327,28 @@ export const DraftItemRow: React.FC = ({ }} bodyStyle={{ padding: 0 }} > - {/* Drag handle - иконка для перетаскивания */} -
{ - e.currentTarget.style.color = "#1890ff"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.color = "#8c8c8c"; - }} - > - -
+ {/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */} + {isReordering && ( +
{ + e.currentTarget.style.color = "#1890ff"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = "#8c8c8c"; + }} + > + +
+ )} diff --git a/rmser-view/src/pages/DraftsList.tsx b/rmser-view/src/pages/DraftsList.tsx index 3e439e0..cf22923 100644 --- a/rmser-view/src/pages/DraftsList.tsx +++ b/rmser-view/src/pages/DraftsList.tsx @@ -1,6 +1,6 @@ // src/pages/DraftsList.tsx -import React, { useState, useMemo, useCallback, useRef } from "react"; +import React, { useState, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { List, @@ -74,9 +74,6 @@ export const DraftsList: React.FC = () => { ); const [endDate, setEndDate] = useState(dayjs()); - const touchStartX = useRef(0); - const touchEndX = useRef(0); - const { data: invoices, isLoading, @@ -181,14 +178,6 @@ export const DraftsList: React.FC = () => { setCurrentPage(1); }; - const handleTouchStart = useCallback((e: React.TouchEvent) => { - touchStartX.current = e.changedTouches[0].screenX; - }, []); - - const handleTouchMove = useCallback((e: React.TouchEvent) => { - touchEndX.current = e.changedTouches[0].screenX; - }, []); - const getItemDate = (item: UnifiedInvoice) => item.type === "DRAFT" ? item.created_at : item.date_incoming; @@ -226,21 +215,6 @@ export const DraftsList: React.FC = () => { return result; }, [invoices, filterType]); - const handleTouchEnd = useCallback(() => { - const diff = touchStartX.current - touchEndX.current; - const totalPages = Math.ceil( - (filteredAndSortedInvoices.length || 0) / pageSize - ); - - if (Math.abs(diff) > 50) { - if (diff > 0 && currentPage < totalPages) { - setCurrentPage((prev) => prev + 1); - } else if (diff < 0 && currentPage > 1) { - setCurrentPage((prev) => prev - 1); - } - } - }, [currentPage, pageSize, filteredAndSortedInvoices]); - const paginatedInvoices = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize); @@ -347,11 +321,7 @@ export const DraftsList: React.FC = () => { ) : ( <> -
+
{Object.entries(groupedInvoices).map(([dateKey, items]) => (
diff --git a/rmser-view/src/pages/InvoiceDraftPage.tsx b/rmser-view/src/pages/InvoiceDraftPage.tsx index 0ec9935..7a6cc14 100644 --- a/rmser-view/src/pages/InvoiceDraftPage.tsx +++ b/rmser-view/src/pages/InvoiceDraftPage.tsx @@ -26,6 +26,7 @@ import { RestOutlined, PlusOutlined, FileImageOutlined, + SwapOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; import { api, getStaticUrl } from "../services/api"; @@ -50,6 +51,7 @@ export const InvoiceDraftPage: React.FC = () => { const [updatingItems, setUpdatingItems] = useState>(new Set()); const [itemsOrder, setItemsOrder] = useState>({}); const [isDragging, setIsDragging] = useState(false); + const [isReordering, setIsReordering] = useState(false); // Состояние для просмотра фото чека const [previewVisible, setPreviewVisible] = useState(false); @@ -386,7 +388,7 @@ export const InvoiceDraftPage: React.FC = () => {
- {/* Правая часть хедера: Кнопка чека и Кнопка удаления */} + {/* Правая часть хедера: Кнопка чека, Кнопка перетаскивания и Кнопка удаления */}
{/* Кнопка просмотра чека (только если есть URL) */} {draft.photo_url && ( @@ -399,6 +401,16 @@ export const InvoiceDraftPage: React.FC = () => { )} + {/* Кнопка переключения режима перетаскивания */} + + + + {currentUserRole === "OWNER" && ( + + + Опасная зона + + deleteAllDraftsMutation.mutate()} + okText="Удалить" + cancelText="Отмена" + okButtonProps={{ danger: true }} + > + + + + )} ); diff --git a/rmser-view/src/services/api.ts b/rmser-view/src/services/api.ts index 5f0bc2a..1364119 100644 --- a/rmser-view/src/services/api.ts +++ b/rmser-view/src/services/api.ts @@ -221,6 +221,11 @@ export const api = { deleteDraft: async (id: string): Promise => { await apiClient.delete(`/drafts/${id}`); }, + + deleteAllDrafts: async (): Promise<{ count: number }> => { + const { data } = await apiClient.delete<{ count: number }>('/drafts'); + return data; + }, // --- Настройки и Статистика ---