2801-опция для перетаскивания строк в черновике.

пофиксил синк накладных
свайп убрал
внешний номер теперь ок
This commit is contained in:
2026-01-28 03:58:43 +03:00
parent 326aabd91d
commit a536b3ff3c
10 changed files with 374 additions and 287 deletions

View File

@@ -573,7 +573,7 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
reqDTO := IncomingInvoiceImportXML{ reqDTO := IncomingInvoiceImportXML{
DocumentNumber: inv.DocumentNumber, DocumentNumber: inv.DocumentNumber,
IncomingDocumentNumber: inv.IncomingDocumentNumber, // Присваиваем входящий номер документа из домена IncomingDocumentNumber: inv.IncomingDocumentNumber,
DateIncoming: inv.DateIncoming.Format("02.01.2006"), DateIncoming: inv.DateIncoming.Format("02.01.2006"),
DefaultStore: inv.DefaultStoreID.String(), DefaultStore: inv.DefaultStoreID.String(),
Supplier: inv.SupplierID.String(), Supplier: inv.SupplierID.String(),
@@ -581,6 +581,13 @@ func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
Comment: comment, 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 { if inv.ID != uuid.Nil {
reqDTO.ID = inv.ID.String() reqDTO.ID = inv.ID.String()
} }

View File

@@ -422,6 +422,14 @@ func (s *Service) CommitDraft(draftID, userID uuid.UUID) (string, error) {
if err != nil { if err != nil {
logger.Log.Warn("Не удалось получить список накладных для поиска UUID", zap.Error(err), zap.Time("date", *draft.DateIncoming)) logger.Log.Warn("Не удалось получить список накладных для поиска UUID", zap.Error(err), zap.Time("date", *draft.DateIncoming))
} else { } 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 found := false
for _, invoice := range invoices { for _, invoice := range invoices {
if invoice.DocumentNumber == docNum { if invoice.DocumentNumber == docNum {

View File

@@ -211,7 +211,7 @@ func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID, force bool) er
return err return err
} }
if lastDate != nil { if lastDate != nil {
from = *lastDate from = lastDate.AddDate(0, 0, -7)
} else { } else {
from = time.Now().AddDate(0, 0, -45) from = time.Now().AddDate(0, 0, -45)
} }

View File

@@ -195,7 +195,7 @@ type CommitRequestDTO struct {
StoreID string `json:"store_id"` StoreID string `json:"store_id"`
SupplierID string `json:"supplier_id"` SupplierID string `json:"supplier_id"`
Comment string `json:"comment"` Comment string `json:"comment"`
IncomingDocNum string `json:"incoming_doc_num"` IncomingDocNum string `json:"incoming_document_number"`
} }
func (h *DraftsHandler) CommitDraft(c *gin.Context) { func (h *DraftsHandler) CommitDraft(c *gin.Context) {

View File

@@ -1,6 +1,6 @@
# =================================================================== # ===================================================================
# Полный контекст React Typescript проекта # Полный контекст React Typescript проекта
# Сгенерировано: 2026-01-27 11:40:29 # Сгенерировано: 2026-01-28 02:48:31
# =================================================================== # ===================================================================
Это полный дамп исходного кода React Typescript (Vite) проекта. Это полный дамп исходного кода React Typescript (Vite) проекта.
@@ -5106,6 +5106,7 @@ interface Props {
onDelete: (itemId: string) => void; onDelete: (itemId: string) => void;
isUpdating: boolean; isUpdating: boolean;
recommendations?: Recommendation[]; recommendations?: Recommendation[];
isReordering: boolean;
} }
type FieldType = "quantity" | "price" | "sum"; type FieldType = "quantity" | "price" | "sum";
@@ -5117,6 +5118,7 @@ export const DraftItemRow: React.FC<Props> = ({
onDelete, onDelete,
isUpdating, isUpdating,
recommendations = [], recommendations = [],
isReordering,
}) => { }) => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -5359,224 +5361,228 @@ export const DraftItemRow: React.FC<Props> = ({
return ( return (
<> <>
<Draggable draggableId={item.id} index={index}> <Draggable draggableId={item.id} index={index} isDragDisabled={!isReordering}>
{(provided, snapshot) => ( {(provided, snapshot) => {
<div const style = {
ref={provided.innerRef} marginBottom: "8px",
{...provided.draggableProps} backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent",
style={{ boxShadow: snapshot.isDragging
...provided.draggableProps.style, ? "0 4px 12px rgba(0, 0, 0, 0.15)"
display: "table", : "none",
width: "100%", borderRadius: "4px",
marginBottom: "8px", transition: "background-color 0.2s ease, box-shadow 0.2s ease",
backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent", ...provided.draggableProps.style,
boxShadow: snapshot.isDragging };
? "0 4px 12px rgba(0, 0, 0, 0.15)"
: "none", return (
borderRadius: "4px", <div
transition: "background-color 0.2s ease, box-shadow 0.2s ease", 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 }}
> >
{/* Drag handle - иконка для перетаскивания */} <Card
<div size="small"
{...provided.dragHandleProps}
style={{ style={{
cursor: "grab",
padding: "4px 8px 4px 0",
color: "#8c8c8c",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
transition: "color 0.2s ease", padding: "12px 16px",
}} borderLeft: `4px solid ${cardBorderColor}`,
onMouseEnter={(e) => { border: snapshot.isDragging
e.currentTarget.style.color = "#1890ff"; ? "2px solid #1890ff"
}} : "1px solid #d9d9d9",
onMouseLeave={(e) => { background: item.product_id ? "#fff" : "#fff1f0",
e.currentTarget.style.color = "#8c8c8c"; borderRadius: "4px",
}} }}
bodyStyle={{ padding: 0 }}
> >
<GripVertical size={20} /> {/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */}
</div> {isReordering && (
<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",
}}
>
{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 <div
{...provided.dragHandleProps}
style={{ style={{
marginLeft: 8, cursor: "grab",
padding: "4px 8px 4px 0",
color: "#8c8c8c",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 6, transition: "color 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "#1890ff";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "#8c8c8c";
}} }}
> >
{isUpdating && ( <GripVertical size={20} />
<SyncOutlined spin style={{ color: "#1890ff" }} />
)}
{activeWarning && (
<WarningFilled
style={{
color: "#faad14",
fontSize: 16,
cursor: "pointer",
}}
onClick={showWarningModal}
/>
)}
{!item.product_id && (
<Tag color="error" style={{ margin: 0 }}>
?
</Tag>
)}
<Popconfirm
title="Удалить строку?"
onConfirm={() => onDelete(item.id)}
okText="Да"
cancelText="Нет"
placement="left"
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
danger
style={{ marginLeft: 4 }}
/>
</Popconfirm>
</div> </div>
</Flex>
<CatalogSelect
value={item.product_id || undefined}
onChange={handleProductChange}
initialProduct={activeProduct}
/>
{activeProduct && (
<Select
style={{ width: "100%" }}
placeholder="Выберите единицу измерения"
options={containerOptions}
value={item.container_id || "BASE_UNIT"}
onChange={handleContainerChange}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "4px 0" }} />
<Button
type="text"
block
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
style={{ textAlign: "left" }}
>
Добавить фасовку...
</Button>
</>
)}
/>
)} )}
<div <Flex vertical gap={10} style={{ flex: 1 }}>
style={{ <Flex justify="space-between" align="start">
display: "flex", <div style={{ flex: 1 }}>
alignItems: "center", <Text
justifyContent: "space-between", type="secondary"
background: "#fafafa", style={{
margin: "0 -12px -12px -12px", fontSize: 12,
padding: "8px 12px", lineHeight: 1.2,
borderTop: "1px solid #f0f0f0", display: "block",
borderBottomLeftRadius: 8, }}
borderBottomRightRadius: 8, >
}} {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" }} />
)}
{activeWarning && (
<WarningFilled
style={{
color: "#faad14",
fontSize: 16,
cursor: "pointer",
}}
onClick={showWarningModal}
/>
)}
{!item.product_id && (
<Tag color="error" style={{ margin: 0 }}>
?
</Tag>
)}
<Popconfirm
title="Удалить строку?"
onConfirm={() => onDelete(item.id)}
okText="Да"
cancelText="Нет"
placement="left"
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
danger
style={{ marginLeft: 4 }}
/>
</Popconfirm>
</div>
</Flex>
<CatalogSelect
value={item.product_id || undefined}
onChange={handleProductChange}
initialProduct={activeProduct}
/>
{activeProduct && (
<Select
style={{ width: "100%" }}
placeholder="Выберите единицу измерения"
options={containerOptions}
value={item.container_id || "BASE_UNIT"}
onChange={handleContainerChange}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "4px 0" }} />
<Button
type="text"
block
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
style={{ textAlign: "left" }}
>
Добавить фасовку...
</Button>
</>
)}
/>
)}
<div <div
style={{ style={{
display: "flex", display: "flex",
gap: 8,
alignItems: "center", alignItems: "center",
flex: 1, justifyContent: "space-between",
background: "#fafafa",
margin: "0 -12px -12px -12px",
padding: "8px 12px",
borderTop: "1px solid #f0f0f0",
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
}} }}
> >
<InputNumber <div
style={{ width: 70 }} style={{
controls={false} display: "flex",
placeholder="Кол" gap: 8,
min={0} alignItems: "center",
value={localQty} flex: 1,
onChange={(val) => handleValueChange("quantity", val)} }}
onBlur={() => handleBlur("quantity")} >
precision={3} <InputNumber
/> style={{ width: 70 }}
<Text type="secondary">x</Text> controls={false}
<InputNumber placeholder="Кол"
style={{ width: 80 }} min={0}
controls={false} value={localQty}
placeholder="Цена" onChange={(val) => handleValueChange("quantity", val)}
min={0} onBlur={() => handleBlur("quantity")}
value={localPrice} precision={3}
onChange={(val) => handleValueChange("price", val)} />
onBlur={() => handleBlur("price")} <Text type="secondary">x</Text>
precision={2} <InputNumber
/> style={{ width: 80 }}
</div> 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 }} style={{ display: "flex", alignItems: "center", gap: 4 }}
> >
<Text type="secondary">=</Text> <Text type="secondary">=</Text>
<InputNumber <InputNumber
style={{ width: 90, fontWeight: "bold" }} style={{ width: 90, fontWeight: "bold" }}
controls={false} controls={false}
placeholder="Сумма" placeholder="Сумма"
min={0} min={0}
value={localSum} value={localSum}
onChange={(val) => handleValueChange("sum", val)} onChange={(val) => handleValueChange("sum", val)}
onBlur={() => handleBlur("sum")} onBlur={() => handleBlur("sum")}
precision={2} precision={2}
/> />
</div>
</div> </div>
</div> </Flex>
</Flex> </Card>
</Card> </div>
</div> );
)} }}
</Draggable> </Draggable>
{activeProduct && ( {activeProduct && (
<CreateContainerModal <CreateContainerModal
@@ -7135,7 +7141,7 @@ export const Dashboard: React.FC = () => {
``` ```
// src/pages/DraftsList.tsx // 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 { useQuery } from "@tanstack/react-query";
import { import {
List, List,
@@ -7209,9 +7215,6 @@ export const DraftsList: React.FC = () => {
); );
const [endDate, setEndDate] = useState<dayjs.Dayjs | null>(dayjs()); const [endDate, setEndDate] = useState<dayjs.Dayjs | null>(dayjs());
const touchStartX = useRef<number>(0);
const touchEndX = useRef<number>(0);
const { const {
data: invoices, data: invoices,
isLoading, isLoading,
@@ -7316,14 +7319,6 @@ export const DraftsList: React.FC = () => {
setCurrentPage(1); 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) => const getItemDate = (item: UnifiedInvoice) =>
item.type === "DRAFT" ? item.created_at : item.date_incoming; item.type === "DRAFT" ? item.created_at : item.date_incoming;
@@ -7361,21 +7356,6 @@ export const DraftsList: React.FC = () => {
return result; return result;
}, [invoices, filterType]); }, [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 paginatedInvoices = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize); return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize);
@@ -7482,11 +7462,7 @@ export const DraftsList: React.FC = () => {
<Empty description="Нет данных" /> <Empty description="Нет данных" />
) : ( ) : (
<> <>
<div <div>
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{Object.entries(groupedInvoices).map(([dateKey, items]) => ( {Object.entries(groupedInvoices).map(([dateKey, items]) => (
<div key={dateKey}> <div key={dateKey}>
<DayDivider date={dateKey} /> <DayDivider date={dateKey} />
@@ -7707,6 +7683,7 @@ import {
RestOutlined, RestOutlined,
PlusOutlined, PlusOutlined,
FileImageOutlined, FileImageOutlined,
SwapOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { api, getStaticUrl } from "../services/api"; import { api, getStaticUrl } from "../services/api";
@@ -7730,6 +7707,8 @@ export const InvoiceDraftPage: React.FC = () => {
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set()); const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
const [itemsOrder, setItemsOrder] = useState<Record<string, number>>({}); const [itemsOrder, setItemsOrder] = useState<Record<string, number>>({});
const [isDragging, setIsDragging] = useState(false);
const [isReordering, setIsReordering] = useState(false);
// Состояние для просмотра фото чека // Состояние для просмотра фото чека
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
@@ -7752,6 +7731,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;
}, },
@@ -7849,6 +7829,7 @@ export const InvoiceDraftPage: React.FC = () => {
}); });
// --- ЭФФЕКТЫ --- // --- ЭФФЕКТЫ ---
useEffect(() => { useEffect(() => {
if (draft) { if (draft) {
const currentValues = form.getFieldsValue(); const currentValues = form.getFieldsValue();
@@ -7939,7 +7920,12 @@ export const InvoiceDraftPage: React.FC = () => {
}); });
}; };
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = async (result: DropResult) => { const handleDragEnd = async (result: DropResult) => {
setIsDragging(false);
const { source, destination } = result; const { source, destination } = result;
// Если нет назначения или позиция не изменилась // Если нет назначения или позиция не изменилась
@@ -8059,7 +8045,7 @@ export const InvoiceDraftPage: React.FC = () => {
</div> </div>
</div> </div>
{/* Правая часть хедера: Кнопка чека и Кнопка удаления */} {/* Правая часть хедера: Кнопка чека, Кнопка перетаскивания и Кнопка удаления */}
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8 }}>
{/* Кнопка просмотра чека (только если есть URL) */} {/* Кнопка просмотра чека (только если есть URL) */}
{draft.photo_url && ( {draft.photo_url && (
@@ -8072,6 +8058,16 @@ export const InvoiceDraftPage: React.FC = () => {
</Button> </Button>
)} )}
{/* Кнопка переключения режима перетаскивания */}
<Button
type={isReordering ? "primary" : "default"}
icon={<SwapOutlined rotate={90} />}
onClick={() => setIsReordering(!isReordering)}
size="small"
>
{isReordering ? "Ок" : ""}
</Button>
<Button <Button
danger={isCanceled} danger={isCanceled}
type={isCanceled ? "primary" : "default"} type={isCanceled ? "primary" : "default"}
@@ -8195,7 +8191,7 @@ export const InvoiceDraftPage: React.FC = () => {
</div> </div>
{/* Items List */} {/* Items List */}
<DragDropContext onDragEnd={handleDragEnd}> <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="draft-items"> <Droppable droppableId="draft-items">
{(provided, snapshot) => ( {(provided, snapshot) => (
<div <div
@@ -8222,6 +8218,7 @@ export const InvoiceDraftPage: React.FC = () => {
onDelete={(itemId) => deleteItemMutation.mutate(itemId)} onDelete={(itemId) => deleteItemMutation.mutate(itemId)}
isUpdating={updatingItems.has(item.id)} isUpdating={updatingItems.has(item.id)}
recommendations={recommendationsQuery.data || []} recommendations={recommendationsQuery.data || []}
isReordering={isReordering}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}
@@ -8700,6 +8697,7 @@ import {
Spin, Spin,
message, message,
Tabs, Tabs,
Popconfirm,
} from "antd"; } from "antd";
import { import {
SaveOutlined, 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(() => { useEffect(() => {
@@ -8871,6 +8880,33 @@ export const SettingsPage: React.FC = () => {
> >
Сохранить настройки Сохранить настройки
</Button> </Button>
{currentUserRole === "OWNER" && (
<Card
size="small"
style={{
marginTop: 24,
borderColor: "#ff4d4f",
borderWidth: 2,
}}
>
<Title level={5} style={{ color: "#ff4d4f", marginBottom: 16 }}>
Опасная зона
</Title>
<Popconfirm
title="Вы уверены?"
description="Это удалит ВСЕ черновики, которые еще не были отправлены в iiko. Это действие необратимо."
onConfirm={() => deleteAllDraftsMutation.mutate()}
okText="Удалить"
cancelText="Отмена"
okButtonProps={{ danger: true }}
>
<Button danger block loading={deleteAllDraftsMutation.isPending}>
Удалить все черновики
</Button>
</Popconfirm>
</Card>
)}
</Form> </Form>
); );
@@ -9194,6 +9230,11 @@ export const api = {
await apiClient.delete(`/drafts/${id}`); await apiClient.delete(`/drafts/${id}`);
}, },
deleteAllDrafts: async (): Promise<{ count: number }> => {
const { data } = await apiClient.delete<{ count: number }>('/drafts');
return data;
},
// --- Настройки и Статистика --- // --- Настройки и Статистика ---
getSettings: async (): Promise<UserSettings> => { getSettings: async (): Promise<UserSettings> => {

View File

@@ -38,6 +38,7 @@ interface Props {
onDelete: (itemId: string) => void; onDelete: (itemId: string) => void;
isUpdating: boolean; isUpdating: boolean;
recommendations?: Recommendation[]; recommendations?: Recommendation[];
isReordering: boolean;
} }
type FieldType = "quantity" | "price" | "sum"; type FieldType = "quantity" | "price" | "sum";
@@ -49,6 +50,7 @@ export const DraftItemRow: React.FC<Props> = ({
onDelete, onDelete,
isUpdating, isUpdating,
recommendations = [], recommendations = [],
isReordering,
}) => { }) => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -291,7 +293,7 @@ export const DraftItemRow: React.FC<Props> = ({
return ( return (
<> <>
<Draggable draggableId={item.id} index={index}> <Draggable draggableId={item.id} index={index} isDragDisabled={!isReordering}>
{(provided, snapshot) => { {(provided, snapshot) => {
const style = { const style = {
marginBottom: "8px", marginBottom: "8px",
@@ -325,26 +327,28 @@ export const DraftItemRow: React.FC<Props> = ({
}} }}
bodyStyle={{ padding: 0 }} bodyStyle={{ padding: 0 }}
> >
{/* Drag handle - иконка для перетаскивания */} {/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */}
<div {isReordering && (
{...provided.dragHandleProps} <div
style={{ {...provided.dragHandleProps}
cursor: "grab", style={{
padding: "4px 8px 4px 0", cursor: "grab",
color: "#8c8c8c", padding: "4px 8px 4px 0",
display: "flex", color: "#8c8c8c",
alignItems: "center", display: "flex",
transition: "color 0.2s ease", alignItems: "center",
}} transition: "color 0.2s ease",
onMouseEnter={(e) => { }}
e.currentTarget.style.color = "#1890ff"; onMouseEnter={(e) => {
}} e.currentTarget.style.color = "#1890ff";
onMouseLeave={(e) => { }}
e.currentTarget.style.color = "#8c8c8c"; onMouseLeave={(e) => {
}} e.currentTarget.style.color = "#8c8c8c";
> }}
<GripVertical size={20} /> >
</div> <GripVertical size={20} />
</div>
)}
<Flex vertical gap={10} style={{ flex: 1 }}> <Flex vertical gap={10} style={{ flex: 1 }}>
<Flex justify="space-between" align="start"> <Flex justify="space-between" align="start">

View File

@@ -1,6 +1,6 @@
// src/pages/DraftsList.tsx // 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 { useQuery } from "@tanstack/react-query";
import { import {
List, List,
@@ -74,9 +74,6 @@ export const DraftsList: React.FC = () => {
); );
const [endDate, setEndDate] = useState<dayjs.Dayjs | null>(dayjs()); const [endDate, setEndDate] = useState<dayjs.Dayjs | null>(dayjs());
const touchStartX = useRef<number>(0);
const touchEndX = useRef<number>(0);
const { const {
data: invoices, data: invoices,
isLoading, isLoading,
@@ -181,14 +178,6 @@ export const DraftsList: React.FC = () => {
setCurrentPage(1); 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) => const getItemDate = (item: UnifiedInvoice) =>
item.type === "DRAFT" ? item.created_at : item.date_incoming; item.type === "DRAFT" ? item.created_at : item.date_incoming;
@@ -226,21 +215,6 @@ export const DraftsList: React.FC = () => {
return result; return result;
}, [invoices, filterType]); }, [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 paginatedInvoices = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize); return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize);
@@ -347,11 +321,7 @@ export const DraftsList: React.FC = () => {
<Empty description="Нет данных" /> <Empty description="Нет данных" />
) : ( ) : (
<> <>
<div <div>
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{Object.entries(groupedInvoices).map(([dateKey, items]) => ( {Object.entries(groupedInvoices).map(([dateKey, items]) => (
<div key={dateKey}> <div key={dateKey}>
<DayDivider date={dateKey} /> <DayDivider date={dateKey} />

View File

@@ -26,6 +26,7 @@ import {
RestOutlined, RestOutlined,
PlusOutlined, PlusOutlined,
FileImageOutlined, FileImageOutlined,
SwapOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { api, getStaticUrl } from "../services/api"; import { api, getStaticUrl } from "../services/api";
@@ -50,6 +51,7 @@ export const InvoiceDraftPage: React.FC = () => {
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set()); const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
const [itemsOrder, setItemsOrder] = useState<Record<string, number>>({}); const [itemsOrder, setItemsOrder] = useState<Record<string, number>>({});
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isReordering, setIsReordering] = useState(false);
// Состояние для просмотра фото чека // Состояние для просмотра фото чека
const [previewVisible, setPreviewVisible] = useState(false); const [previewVisible, setPreviewVisible] = useState(false);
@@ -386,7 +388,7 @@ export const InvoiceDraftPage: React.FC = () => {
</div> </div>
</div> </div>
{/* Правая часть хедера: Кнопка чека и Кнопка удаления */} {/* Правая часть хедера: Кнопка чека, Кнопка перетаскивания и Кнопка удаления */}
<div style={{ display: "flex", gap: 8 }}> <div style={{ display: "flex", gap: 8 }}>
{/* Кнопка просмотра чека (только если есть URL) */} {/* Кнопка просмотра чека (только если есть URL) */}
{draft.photo_url && ( {draft.photo_url && (
@@ -399,6 +401,16 @@ export const InvoiceDraftPage: React.FC = () => {
</Button> </Button>
)} )}
{/* Кнопка переключения режима перетаскивания */}
<Button
type={isReordering ? "primary" : "default"}
icon={<SwapOutlined rotate={90} />}
onClick={() => setIsReordering(!isReordering)}
size="small"
>
{isReordering ? "Ок" : ""}
</Button>
<Button <Button
danger={isCanceled} danger={isCanceled}
type={isCanceled ? "primary" : "default"} type={isCanceled ? "primary" : "default"}
@@ -549,6 +561,7 @@ export const InvoiceDraftPage: React.FC = () => {
onDelete={(itemId) => deleteItemMutation.mutate(itemId)} onDelete={(itemId) => deleteItemMutation.mutate(itemId)}
isUpdating={updatingItems.has(item.id)} isUpdating={updatingItems.has(item.id)}
recommendations={recommendationsQuery.data || []} recommendations={recommendationsQuery.data || []}
isReordering={isReordering}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}

View File

@@ -13,6 +13,7 @@ import {
Spin, Spin,
message, message,
Tabs, Tabs,
Popconfirm,
} from "antd"; } from "antd";
import { import {
SaveOutlined, SaveOutlined,
@@ -71,6 +72,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(() => { useEffect(() => {
@@ -184,6 +196,33 @@ export const SettingsPage: React.FC = () => {
> >
Сохранить настройки Сохранить настройки
</Button> </Button>
{currentUserRole === "OWNER" && (
<Card
size="small"
style={{
marginTop: 24,
borderColor: "#ff4d4f",
borderWidth: 2,
}}
>
<Title level={5} style={{ color: "#ff4d4f", marginBottom: 16 }}>
Опасная зона
</Title>
<Popconfirm
title="Вы уверены?"
description="Это удалит ВСЕ черновики, которые еще не были отправлены в iiko. Это действие необратимо."
onConfirm={() => deleteAllDraftsMutation.mutate()}
okText="Удалить"
cancelText="Отмена"
okButtonProps={{ danger: true }}
>
<Button danger block loading={deleteAllDraftsMutation.isPending}>
Удалить все черновики
</Button>
</Popconfirm>
</Card>
)}
</Form> </Form>
); );

View File

@@ -222,6 +222,11 @@ export const api = {
await apiClient.delete(`/drafts/${id}`); await apiClient.delete(`/drafts/${id}`);
}, },
deleteAllDrafts: async (): Promise<{ count: number }> => {
const { data } = await apiClient.delete<{ count: number }>('/drafts');
return data;
},
// --- Настройки и Статистика --- // --- Настройки и Статистика ---
getSettings: async (): Promise<UserSettings> => { getSettings: async (): Promise<UserSettings> => {