mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
пересчет поправил редактирование с перепроведением галка автопроведения работает рекомендации починил
777 lines
24 KiB
TypeScript
777 lines
24 KiB
TypeScript
import React, { useEffect, useMemo, useState, useRef } from "react";
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||
import {
|
||
Spin,
|
||
Alert,
|
||
Button,
|
||
Form,
|
||
Select,
|
||
DatePicker,
|
||
Input,
|
||
Typography,
|
||
message,
|
||
Row,
|
||
Col,
|
||
Modal,
|
||
Tag,
|
||
Image,
|
||
Checkbox,
|
||
} from "antd";
|
||
import {
|
||
// CheckOutlined,
|
||
DeleteOutlined,
|
||
ExclamationCircleFilled,
|
||
StopOutlined,
|
||
ArrowLeftOutlined,
|
||
PlusOutlined,
|
||
FileImageOutlined,
|
||
FileExcelOutlined,
|
||
SwapOutlined,
|
||
} from "@ant-design/icons";
|
||
import dayjs from "dayjs";
|
||
import { api, getStaticUrl } from "../../services/api";
|
||
import { DraftItemRow } from "./DraftItemRow";
|
||
import ExcelPreviewModal from "../common/ExcelPreviewModal";
|
||
import { useActiveDraftStore } from "../../stores/activeDraftStore";
|
||
import type {
|
||
DraftItem,
|
||
UpdateDraftRequest,
|
||
CommitDraftRequest,
|
||
ReorderDraftItemsRequest,
|
||
} from "../../services/types";
|
||
import { DragDropContext, Droppable, type DropResult } from "@hello-pangea/dnd";
|
||
|
||
const { Text } = Typography;
|
||
const { TextArea } = Input;
|
||
const { confirm } = Modal;
|
||
|
||
interface DraftEditorProps {
|
||
draftId: string;
|
||
onBack?: () => void;
|
||
}
|
||
|
||
export const DraftEditor: React.FC<DraftEditorProps> = ({
|
||
draftId,
|
||
onBack,
|
||
}) => {
|
||
const queryClient = useQueryClient();
|
||
const [form] = Form.useForm();
|
||
|
||
// Zustand стор для локального управления элементами черновика
|
||
const {
|
||
items,
|
||
isDirty,
|
||
setItems,
|
||
updateItem,
|
||
deleteItem,
|
||
addItem,
|
||
reorderItems,
|
||
resetDirty,
|
||
markAsDirty,
|
||
} = useActiveDraftStore();
|
||
|
||
// Отслеживаем текущий draftId для инициализации стора
|
||
const currentDraftIdRef = useRef<string | null>(null);
|
||
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [isReordering, setIsReordering] = useState(false);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
|
||
// Состояние для просмотра фото чека
|
||
const [previewVisible, setPreviewVisible] = useState(false);
|
||
|
||
// Состояние для просмотра Excel файла
|
||
const [excelPreviewVisible, setExcelPreviewVisible] = useState(false);
|
||
|
||
// Состояние для чекбокса "Проведено"
|
||
const [isProcessed, setIsProcessed] = useState(true); // По умолчанию true для MVP
|
||
|
||
// --- ЗАПРОСЫ ---
|
||
|
||
const dictQuery = useQuery({
|
||
queryKey: ["dictionaries"],
|
||
queryFn: api.getDictionaries,
|
||
staleTime: 1000 * 60 * 5,
|
||
});
|
||
|
||
const recommendationsQuery = useQuery({
|
||
queryKey: ["recommendations"],
|
||
queryFn: api.getRecommendations,
|
||
});
|
||
|
||
const draftQuery = useQuery({
|
||
queryKey: ["draft", draftId],
|
||
queryFn: () => api.getDraft(draftId),
|
||
enabled: !!draftId,
|
||
refetchInterval: (query) => {
|
||
if (isDragging) return false;
|
||
const status = query.state.data?.status;
|
||
return status === "PROCESSING" ? 3000 : false;
|
||
},
|
||
});
|
||
|
||
const draft = draftQuery.data;
|
||
const stores = dictQuery.data?.stores || [];
|
||
const suppliers = dictQuery.data?.suppliers || [];
|
||
|
||
// Определение типа файла по расширению
|
||
const isExcelFile = draft?.photo_url?.toLowerCase().match(/\.(xls|xlsx)$/);
|
||
|
||
// --- МУТАЦИИ ---
|
||
|
||
const addItemMutation = useMutation({
|
||
mutationFn: () => api.addDraftItem(draftId),
|
||
onSuccess: (newItem) => {
|
||
message.success("Строка добавлена");
|
||
// Добавляем новый элемент в стор
|
||
addItem(newItem);
|
||
},
|
||
onError: () => {
|
||
message.error("Ошибка создания строки");
|
||
},
|
||
});
|
||
|
||
const commitMutation = useMutation({
|
||
mutationFn: (payload: CommitDraftRequest) =>
|
||
api.commitDraft(draftId, payload),
|
||
onSuccess: (data) => {
|
||
message.success(`Накладная ${data.document_number} создана!`);
|
||
queryClient.invalidateQueries({ queryKey: ["drafts"] });
|
||
},
|
||
onError: () => {
|
||
message.error("Ошибка при создании накладной");
|
||
},
|
||
});
|
||
|
||
const deleteDraftMutation = useMutation({
|
||
mutationFn: () => api.deleteDraft(draftId),
|
||
onSuccess: () => {
|
||
if (draft?.status === "CANCELED") {
|
||
message.info("Черновик удален окончательно");
|
||
} else {
|
||
message.warning("Черновик отменен");
|
||
queryClient.invalidateQueries({ queryKey: ["draft", draftId] });
|
||
}
|
||
},
|
||
onError: () => {
|
||
message.error("Ошибка при удалении");
|
||
},
|
||
});
|
||
|
||
const reorderItemsMutation = useMutation({
|
||
mutationFn: ({
|
||
draftId: id,
|
||
payload,
|
||
}: {
|
||
draftId: string;
|
||
payload: ReorderDraftItemsRequest;
|
||
}) => api.reorderDraftItems(id, payload),
|
||
onError: (error) => {
|
||
message.error("Не удалось изменить порядок элементов");
|
||
console.error("Reorder error:", error);
|
||
},
|
||
});
|
||
|
||
// --- ЭФФЕКТЫ ---
|
||
|
||
// Инициализация данных при загрузке черновика
|
||
useEffect(() => {
|
||
if (draft) {
|
||
// Инициализируем только если изменился draftId или стор пуст
|
||
if (currentDraftIdRef.current !== draft.id || items.length === 0) {
|
||
// 1. Инициализация строк (Store)
|
||
setItems(draft.items || []);
|
||
|
||
// 2. Инициализация шапки (Form)
|
||
form.setFieldsValue({
|
||
store_id: draft.store_id,
|
||
supplier_id: draft.supplier_id,
|
||
comment: draft.comment,
|
||
incoming_document_number: draft.incoming_document_number,
|
||
date_incoming: draft.date_incoming
|
||
? dayjs(draft.date_incoming)
|
||
: dayjs(),
|
||
});
|
||
|
||
currentDraftIdRef.current = draft.id;
|
||
}
|
||
}
|
||
}, [draft, items.length, setItems, form]);
|
||
|
||
// --- ХЕЛПЕРЫ ---
|
||
const totalSum = useMemo(() => {
|
||
return (
|
||
items.reduce(
|
||
(acc, item) => acc + Number(item.quantity) * Number(item.price),
|
||
0
|
||
) || 0
|
||
);
|
||
}, [items]);
|
||
|
||
const invalidItemsCount = useMemo(() => {
|
||
return items.filter((i) => !i.product_id).length || 0;
|
||
}, [items]);
|
||
|
||
// Функция сохранения изменений на сервер
|
||
const saveChanges = async () => {
|
||
if (!isDirty) return;
|
||
|
||
setIsSaving(true);
|
||
try {
|
||
// Собираем значения формы для обновления шапки черновика
|
||
const formValues = form.getFieldsValue();
|
||
|
||
// Формируем единый payload для пакетного обновления (шапка + элементы)
|
||
const payload: UpdateDraftRequest = {
|
||
store_id: formValues.store_id,
|
||
supplier_id: formValues.supplier_id,
|
||
comment: formValues.comment || "",
|
||
incoming_document_number: formValues.incoming_document_number || "",
|
||
date_incoming: formValues.date_incoming
|
||
? formValues.date_incoming.format("YYYY-MM-DD")
|
||
: undefined,
|
||
items: items.map((item) => ({
|
||
id: item.id,
|
||
product_id: item.product_id ?? "",
|
||
container_id: item.container_id ?? "",
|
||
quantity: Number(item.quantity),
|
||
price: Number(item.price),
|
||
sum: Number(item.sum),
|
||
})),
|
||
};
|
||
|
||
// Отправляем единый запрос на сервер
|
||
await api.updateDraft(draftId, payload);
|
||
|
||
// После успешного сохранения обновляем данные с сервера
|
||
await queryClient.invalidateQueries({ queryKey: ["draft", draftId] });
|
||
|
||
// Сбрасываем флаг isDirty
|
||
resetDirty();
|
||
|
||
message.success("Изменения сохранены");
|
||
} catch (error) {
|
||
console.error("Ошибка сохранения:", error);
|
||
message.error("Не удалось сохранить изменения");
|
||
throw error;
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleItemUpdate = (id: string, changes: Partial<DraftItem>) => {
|
||
// Обновляем локально через стор
|
||
updateItem(id, changes);
|
||
};
|
||
|
||
const handleCommit = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
|
||
if (invalidItemsCount > 0) {
|
||
message.warning(
|
||
`Осталось ${invalidItemsCount} нераспознанных товаров!`
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Сначала сохраняем изменения, если есть
|
||
if (isDirty) {
|
||
await saveChanges();
|
||
}
|
||
|
||
commitMutation.mutate({
|
||
date_incoming: values.date_incoming.format("YYYY-MM-DD"),
|
||
store_id: values.store_id,
|
||
supplier_id: values.supplier_id,
|
||
comment: values.comment || "",
|
||
incoming_document_number: values.incoming_document_number || "",
|
||
is_processed: isProcessed,
|
||
});
|
||
} catch {
|
||
message.error("Заполните обязательные поля (Склад, Поставщик)");
|
||
}
|
||
};
|
||
|
||
const isCanceled = draft?.status === "CANCELED";
|
||
const isCompleted = draft?.status === "COMPLETED";
|
||
|
||
const handleBack = () => {
|
||
if (isDirty) {
|
||
confirm({
|
||
title: "Сохранить изменения?",
|
||
icon: <ExclamationCircleFilled style={{ color: "#1890ff" }} />,
|
||
content:
|
||
"У вас есть несохраненные изменения. Сохранить их перед выходом?",
|
||
okText: "Сохранить и выйти",
|
||
cancelText: "Выйти без сохранения",
|
||
onOk: async () => {
|
||
try {
|
||
await saveChanges();
|
||
onBack?.();
|
||
} catch {
|
||
// Ошибка уже обработана в saveChanges
|
||
}
|
||
},
|
||
onCancel: () => {
|
||
onBack?.();
|
||
},
|
||
});
|
||
} else {
|
||
onBack?.();
|
||
}
|
||
};
|
||
|
||
const handleDelete = () => {
|
||
confirm({
|
||
title: isCanceled ? "Удалить окончательно?" : "Отменить черновик?",
|
||
icon: <ExclamationCircleFilled style={{ color: "red" }} />,
|
||
content: isCanceled
|
||
? "Черновик пропадет из списка навсегда."
|
||
: 'Черновик получит статус "Отменен", но останется в списке.',
|
||
okText: isCanceled ? "Удалить навсегда" : "Отменить",
|
||
okType: "danger",
|
||
cancelText: "Назад",
|
||
onOk() {
|
||
deleteDraftMutation.mutate();
|
||
},
|
||
});
|
||
};
|
||
|
||
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;
|
||
|
||
// Обновляем локальное состояние через стор
|
||
reorderItems(source.index, destination.index);
|
||
|
||
// Подготавливаем payload для API
|
||
const reorderPayload: ReorderDraftItemsRequest = {
|
||
items: items.map((item, index) => ({
|
||
id: item.id,
|
||
order: index,
|
||
})),
|
||
};
|
||
|
||
// Отправляем запрос на сервер
|
||
try {
|
||
await reorderItemsMutation.mutateAsync({
|
||
draftId: draft.id,
|
||
payload: reorderPayload,
|
||
});
|
||
} catch {
|
||
// При ошибке откатываем локальное состояние через стор
|
||
reorderItems(destination.index, source.index);
|
||
message.error("Не удалось изменить порядок элементов");
|
||
}
|
||
};
|
||
|
||
// --- RENDER ---
|
||
const showSpinner =
|
||
draftQuery.isLoading ||
|
||
(draft?.status === "PROCESSING" && items.length === 0);
|
||
|
||
if (showSpinner) {
|
||
return (
|
||
<div style={{ textAlign: "center", padding: 50 }}>
|
||
<Spin size="large" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (draftQuery.isError || !draft) {
|
||
return <Alert type="error" message="Ошибка загрузки черновика" />;
|
||
}
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
height: "100%",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
fontFamily:
|
||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
|
||
backgroundColor: "#f5f5f5",
|
||
}}
|
||
>
|
||
{/* Единый хедер */}
|
||
<div
|
||
style={{
|
||
padding: "8px 12px",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
gap: 8,
|
||
flexShrink: 0,
|
||
background: "#fff",
|
||
borderBottom: "1px solid #f0f0f0",
|
||
}}
|
||
>
|
||
{/* Левая часть: Кнопка назад + Номер + Статус */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
flex: 1,
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
{onBack && (
|
||
<Button
|
||
type="text"
|
||
icon={<ArrowLeftOutlined />}
|
||
onClick={handleBack}
|
||
size="small"
|
||
style={{ flexShrink: 0 }}
|
||
/>
|
||
)}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
flexWrap: "wrap",
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
<Text
|
||
style={{ fontSize: 16, fontWeight: "bold", whiteSpace: "nowrap" }}
|
||
>
|
||
{draft.document_number ? `№${draft.document_number}` : "Черновик"}
|
||
</Text>
|
||
{draft.status === "PROCESSING" && <Spin size="small" />}
|
||
{isCanceled && (
|
||
<Tag color="red" style={{ margin: 0, fontSize: 11 }}>
|
||
ОТМЕНЕН
|
||
</Tag>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Правая часть: Кнопки действий */}
|
||
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
|
||
{/* Кнопка сохранения (показывается только если есть несохраненные изменения) */}
|
||
{isDirty && (
|
||
<Button
|
||
type="primary"
|
||
onClick={saveChanges}
|
||
loading={isSaving}
|
||
size="small"
|
||
>
|
||
Сохранить
|
||
</Button>
|
||
)}
|
||
|
||
{/* Кнопка просмотра чека (только если есть URL) */}
|
||
{draft.photo_url && (
|
||
<Button
|
||
icon={isExcelFile ? <FileExcelOutlined /> : <FileImageOutlined />}
|
||
onClick={() =>
|
||
isExcelFile
|
||
? setExcelPreviewVisible(true)
|
||
: setPreviewVisible(true)
|
||
}
|
||
size="small"
|
||
type="text"
|
||
/>
|
||
)}
|
||
|
||
{/* Кнопка переключения режима перетаскивания */}
|
||
<Button
|
||
type={isReordering ? "primary" : "default"}
|
||
icon={<SwapOutlined rotate={90} />}
|
||
onClick={() => setIsReordering(!isReordering)}
|
||
size="small"
|
||
/>
|
||
|
||
{/* Кнопка удаления/отмены */}
|
||
<Button
|
||
danger={isCanceled}
|
||
type={isCanceled ? "primary" : "default"}
|
||
icon={isCanceled ? <DeleteOutlined /> : <StopOutlined />}
|
||
onClick={handleDelete}
|
||
loading={deleteDraftMutation.isPending}
|
||
size="small"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Основная часть с прокруткой */}
|
||
<div style={{ flex: 1, overflowY: "auto", padding: "8px 12px" }}>
|
||
{/* Form: Склады и Поставщики */}
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
padding: 6,
|
||
borderRadius: 8,
|
||
marginBottom: 12,
|
||
opacity: isCanceled ? 0.6 : 1,
|
||
}}
|
||
>
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onValuesChange={() => markAsDirty()}
|
||
>
|
||
<Row gutter={[4, 4]}>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="date_incoming"
|
||
label="Дата"
|
||
rules={[{ required: true }]}
|
||
style={{ marginBottom: 0 }}
|
||
>
|
||
<DatePicker
|
||
style={{ width: "100%" }}
|
||
format="DD.MM.YYYY"
|
||
placeholder=""
|
||
size="small"
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
<Col span={12}>
|
||
<Form.Item
|
||
name="incoming_document_number"
|
||
label="№ входящего"
|
||
style={{ marginBottom: 0 }}
|
||
>
|
||
<Input placeholder="" size="small" />
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
<Row gutter={[4, 4]}>
|
||
<Col span={24}>
|
||
<Form.Item
|
||
name="store_id"
|
||
rules={[{ required: true, message: "Выберите склад" }]}
|
||
style={{ marginBottom: 0 }}
|
||
>
|
||
<Select
|
||
placeholder="Выберите склад..."
|
||
loading={dictQuery.isLoading}
|
||
options={stores.map((s) => ({
|
||
label: s.name,
|
||
value: s.id,
|
||
}))}
|
||
size="small"
|
||
/>
|
||
</Form.Item>
|
||
</Col>
|
||
</Row>
|
||
<Form.Item
|
||
name="supplier_id"
|
||
rules={[{ required: true, message: "Выберите поставщика" }]}
|
||
style={{ marginBottom: 8 }}
|
||
>
|
||
<Select
|
||
placeholder="Поставщик..."
|
||
loading={dictQuery.isLoading}
|
||
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
|
||
size="small"
|
||
showSearch
|
||
filterOption={(input, option) =>
|
||
(option?.label ?? "")
|
||
.toLowerCase()
|
||
.includes(input.toLowerCase())
|
||
}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="comment" style={{ marginBottom: 0 }}>
|
||
<TextArea
|
||
rows={1}
|
||
placeholder="Комментарий..."
|
||
style={{ fontSize: 13 }}
|
||
size="small"
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
</div>
|
||
|
||
{/* Items Header */}
|
||
<div
|
||
style={{
|
||
marginBottom: 8,
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
padding: "0 4px",
|
||
}}
|
||
>
|
||
<Text strong style={{ fontSize: 14 }}>
|
||
Позиции ({items.length})
|
||
</Text>
|
||
{invalidItemsCount > 0 && (
|
||
<Text type="danger" style={{ fontSize: 12 }}>
|
||
{invalidItemsCount} нераспознано
|
||
</Text>
|
||
)}
|
||
</div>
|
||
|
||
{/* Items List */}
|
||
<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",
|
||
}}
|
||
>
|
||
{items.map((item, index) => (
|
||
<DraftItemRow
|
||
key={item.id}
|
||
item={item}
|
||
index={index}
|
||
onLocalUpdate={handleItemUpdate}
|
||
onDelete={(itemId) => deleteItem(itemId)}
|
||
isUpdating={false}
|
||
recommendations={recommendationsQuery.data || []}
|
||
isReordering={isReordering}
|
||
/>
|
||
))}
|
||
{provided.placeholder}
|
||
</div>
|
||
)}
|
||
</Droppable>
|
||
</DragDropContext>
|
||
|
||
{/* Кнопка добавления позиции */}
|
||
<Button
|
||
type="dashed"
|
||
block
|
||
icon={<PlusOutlined />}
|
||
style={{ marginTop: 12, marginBottom: 80, height: 40 }}
|
||
onClick={() => addItemMutation.mutate()}
|
||
loading={addItemMutation.isPending}
|
||
disabled={isCanceled}
|
||
size="small"
|
||
>
|
||
Добавить товар
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Footer Actions - прижат к низу контейнера */}
|
||
<div
|
||
style={{
|
||
flexShrink: 0,
|
||
background: "#fff",
|
||
padding: "8px 12px",
|
||
borderTop: "1px solid #f0f0f0",
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||
<Text style={{ fontSize: 11, color: "#888", lineHeight: 1 }}>
|
||
Итого:
|
||
</Text>
|
||
<Text
|
||
style={{
|
||
fontSize: 16,
|
||
fontWeight: "bold",
|
||
color: "#1890ff",
|
||
lineHeight: 1.2,
|
||
}}
|
||
>
|
||
{totalSum.toLocaleString("ru-RU", {
|
||
style: "currency",
|
||
currency: "RUB",
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
})}
|
||
</Text>
|
||
</div>
|
||
|
||
<Button
|
||
type="primary"
|
||
// icon={<CheckOutlined />}
|
||
onClick={handleCommit}
|
||
loading={commitMutation.isPending}
|
||
disabled={invalidItemsCount > 0 || isCanceled}
|
||
style={{
|
||
position: "relative",
|
||
paddingLeft: 40,
|
||
height: 36,
|
||
}}
|
||
size="small"
|
||
>
|
||
<Checkbox
|
||
checked={isProcessed}
|
||
onChange={(e) => setIsProcessed(e.target.checked)}
|
||
onClick={(e) => e.stopPropagation()}
|
||
disabled={invalidItemsCount > 0 || isCanceled}
|
||
style={{
|
||
position: "absolute",
|
||
left: 10,
|
||
top: "50%",
|
||
transform: "translateY(-50%)",
|
||
pointerEvents: "auto",
|
||
}}
|
||
/>
|
||
<span style={{ marginLeft: 8 }}>
|
||
{isCanceled
|
||
? "Восстановить"
|
||
: isCompleted
|
||
? "Обновить в iiko"
|
||
: isProcessed
|
||
? "Провести и отправить"
|
||
: "Сохранить (без проведения)"}
|
||
</span>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Скрытый компонент для просмотра изображения */}
|
||
{draft.photo_url && (
|
||
<div style={{ display: "none" }}>
|
||
<Image.PreviewGroup
|
||
preview={{
|
||
visible: previewVisible,
|
||
onVisibleChange: (vis) => setPreviewVisible(vis),
|
||
movable: true,
|
||
scaleStep: 0.5,
|
||
}}
|
||
>
|
||
<Image src={getStaticUrl(draft.photo_url)} />
|
||
</Image.PreviewGroup>
|
||
</div>
|
||
)}
|
||
|
||
{/* Модальное окно для просмотра Excel файлов */}
|
||
<ExcelPreviewModal
|
||
visible={excelPreviewVisible}
|
||
onCancel={() => setExcelPreviewVisible(false)}
|
||
fileUrl={draft.photo_url || ""}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|