mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
315 lines
9.3 KiB
TypeScript
315 lines
9.3 KiB
TypeScript
// src/pages/DraftsList.tsx
|
||
|
||
import React, { useState } from "react";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import {
|
||
List,
|
||
Typography,
|
||
Tag,
|
||
Spin,
|
||
Empty,
|
||
DatePicker,
|
||
Flex,
|
||
Button,
|
||
} from "antd";
|
||
import { useNavigate } from "react-router-dom";
|
||
import {
|
||
ArrowRightOutlined,
|
||
CheckCircleOutlined,
|
||
DeleteOutlined,
|
||
PlusOutlined,
|
||
ExclamationCircleOutlined,
|
||
LoadingOutlined,
|
||
CloseCircleOutlined,
|
||
StopOutlined,
|
||
SyncOutlined,
|
||
CloudServerOutlined,
|
||
} from "@ant-design/icons";
|
||
import dayjs, { Dayjs } from "dayjs";
|
||
import { api } from "../services/api";
|
||
import type { UnifiedInvoice } from "../services/types";
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
export const DraftsList: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
|
||
// Состояние фильтра дат: по умолчанию последние 7 дней
|
||
const [startDate, setStartDate] = useState<Dayjs>(dayjs().subtract(7, "day"));
|
||
const [endDate, setEndDate] = useState<Dayjs>(dayjs());
|
||
const [syncLoading, setSyncLoading] = useState(false);
|
||
|
||
// Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос)
|
||
const {
|
||
data: invoices,
|
||
isLoading,
|
||
isError,
|
||
refetch,
|
||
} = useQuery({
|
||
queryKey: [
|
||
"drafts",
|
||
startDate.format("YYYY-MM-DD"),
|
||
endDate.format("YYYY-MM-DD"),
|
||
],
|
||
queryFn: () =>
|
||
api.getDrafts(
|
||
startDate.format("YYYY-MM-DD"),
|
||
endDate.format("YYYY-MM-DD")
|
||
),
|
||
staleTime: 0,
|
||
refetchOnMount: true,
|
||
refetchOnWindowFocus: true,
|
||
});
|
||
|
||
const handleSync = async () => {
|
||
setSyncLoading(true);
|
||
try {
|
||
await api.syncInvoices();
|
||
refetch();
|
||
} finally {
|
||
setSyncLoading(false);
|
||
}
|
||
};
|
||
|
||
const getStatusTag = (item: UnifiedInvoice) => {
|
||
switch (item.status) {
|
||
case "PROCESSING":
|
||
return (
|
||
<Tag icon={<LoadingOutlined />} color="blue">
|
||
Обработка
|
||
</Tag>
|
||
);
|
||
case "READY_TO_VERIFY":
|
||
return (
|
||
<Tag icon={<ExclamationCircleOutlined />} color="orange">
|
||
Проверка
|
||
</Tag>
|
||
);
|
||
case "COMPLETED":
|
||
return (
|
||
<Tag icon={<CheckCircleOutlined />} color="green">
|
||
Готово
|
||
</Tag>
|
||
);
|
||
case "ERROR":
|
||
return (
|
||
<Tag icon={<CloseCircleOutlined />} color="red">
|
||
Ошибка
|
||
</Tag>
|
||
);
|
||
case "CANCELED":
|
||
return (
|
||
<Tag icon={<StopOutlined />} color="default">
|
||
Отменен
|
||
</Tag>
|
||
);
|
||
case "NEW":
|
||
return (
|
||
<Tag icon={<PlusOutlined />} color="blue">
|
||
Новая
|
||
</Tag>
|
||
);
|
||
case "PROCESSED":
|
||
return (
|
||
<Tag icon={<CheckCircleOutlined />} color="green">
|
||
Проведена
|
||
</Tag>
|
||
);
|
||
case "DELETED":
|
||
return (
|
||
<Tag icon={<DeleteOutlined />} color="red">
|
||
Удалена
|
||
</Tag>
|
||
);
|
||
default:
|
||
return <Tag>{item.status}</Tag>;
|
||
}
|
||
};
|
||
|
||
const handleInvoiceClick = (item: UnifiedInvoice) => {
|
||
if (item.type === "DRAFT") {
|
||
navigate("/invoice/draft/" + item.id);
|
||
} else if (item.type === "SYNCED") {
|
||
navigate("/invoice/view/" + item.id);
|
||
}
|
||
};
|
||
|
||
if (isError) {
|
||
return (
|
||
<div style={{ padding: 20 }}>
|
||
<Text type="danger">Ошибка загрузки списка накладных</Text>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{ padding: "0 4px 20px" }}>
|
||
<Flex align="center" gap={8} style={{ marginTop: 16, marginBottom: 16 }}>
|
||
<Title level={4} style={{ margin: 0 }}>
|
||
Накладные
|
||
</Title>
|
||
<Button
|
||
icon={<SyncOutlined />}
|
||
loading={syncLoading}
|
||
onClick={handleSync}
|
||
/>
|
||
</Flex>
|
||
|
||
{/* Фильтр дат */}
|
||
<div
|
||
style={{
|
||
marginBottom: 16,
|
||
background: "#fff",
|
||
padding: 12,
|
||
borderRadius: 8,
|
||
}}
|
||
>
|
||
<Text
|
||
type="secondary"
|
||
style={{ display: "block", marginBottom: 8, fontSize: 12 }}
|
||
>
|
||
Период загрузки:
|
||
</Text>
|
||
<Flex gap={8}>
|
||
<DatePicker
|
||
value={startDate}
|
||
onChange={(date) => date && setStartDate(date)}
|
||
style={{ flex: 1 }}
|
||
placeholder="Начало"
|
||
format="DD.MM.YYYY"
|
||
allowClear={false}
|
||
/>
|
||
<DatePicker
|
||
value={endDate}
|
||
onChange={(date) => date && setEndDate(date)}
|
||
style={{ flex: 1 }}
|
||
placeholder="Конец"
|
||
format="DD.MM.YYYY"
|
||
allowClear={false}
|
||
/>
|
||
</Flex>
|
||
</div>
|
||
|
||
{isLoading ? (
|
||
<div style={{ textAlign: "center", padding: 40 }}>
|
||
<Spin size="large" />
|
||
</div>
|
||
) : !invoices || invoices.length === 0 ? (
|
||
<Empty description="Нет данных за выбранный период" />
|
||
) : (
|
||
<List
|
||
dataSource={invoices}
|
||
renderItem={(item) => {
|
||
const isSynced = item.type === "SYNCED";
|
||
const displayDate =
|
||
item.type === "DRAFT" ? item.created_at : item.date_incoming;
|
||
|
||
return (
|
||
<List.Item
|
||
style={{
|
||
background: isSynced ? "#fafafa" : "#fff",
|
||
padding: 12,
|
||
marginBottom: 10,
|
||
borderRadius: 12,
|
||
cursor: "pointer",
|
||
border: isSynced ? "1px solid #f0f0f0" : "1px solid #e6f7ff",
|
||
boxShadow: "0 2px 4px rgba(0,0,0,0.02)",
|
||
display: "block",
|
||
}}
|
||
onClick={() => handleInvoiceClick(item)}
|
||
>
|
||
<Flex vertical gap={4}>
|
||
<Flex justify="space-between" align="start">
|
||
<Flex vertical>
|
||
<Flex align="center" gap={8}>
|
||
<Text strong style={{ fontSize: 16 }}>
|
||
{item.document_number || "Без номера"}
|
||
</Text>
|
||
{item.type === "SYNCED" && (
|
||
<CloudServerOutlined style={{ color: "gray" }} />
|
||
)}
|
||
{item.is_app_created && (
|
||
<span title="Создано в RMSer">📱</span>
|
||
)}
|
||
</Flex>
|
||
<Flex vertical gap={2}>
|
||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||
{item.items_count} поз.
|
||
</Text>
|
||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||
{dayjs(displayDate).format("DD.MM.YYYY")}
|
||
</Text>
|
||
</Flex>
|
||
{item.incoming_number && (
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
Вх. № {item.incoming_number}
|
||
</Text>
|
||
)}
|
||
</Flex>
|
||
<Flex vertical align="start" gap={4}>
|
||
{getStatusTag(item)}
|
||
{isSynced && item.items_preview && (
|
||
<div>
|
||
{item.items_preview
|
||
.split(", ")
|
||
.map((previewItem, idx) => (
|
||
<div
|
||
key={idx}
|
||
style={{ fontSize: 12, color: "#666" }}
|
||
>
|
||
{previewItem}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Flex>
|
||
</Flex>
|
||
|
||
<Flex justify="space-between" align="center">
|
||
<div></div>
|
||
{item.store_name && (
|
||
<Tag
|
||
style={{
|
||
margin: 0,
|
||
maxWidth: 120,
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
}}
|
||
>
|
||
{item.store_name}
|
||
</Tag>
|
||
)}
|
||
</Flex>
|
||
|
||
<Flex
|
||
justify="space-between"
|
||
align="center"
|
||
style={{ marginTop: 8 }}
|
||
>
|
||
<Text
|
||
strong
|
||
style={{
|
||
fontSize: 17,
|
||
color: isSynced ? "#595959" : "#1890ff",
|
||
}}
|
||
>
|
||
{item.total_sum.toLocaleString("ru-RU", {
|
||
style: "currency",
|
||
currency: "RUB",
|
||
maximumFractionDigits: 0,
|
||
})}
|
||
</Text>
|
||
{!isSynced && (
|
||
<ArrowRightOutlined style={{ color: "#1890ff" }} />
|
||
)}
|
||
</Flex>
|
||
</Flex>
|
||
</List.Item>
|
||
);
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|