пофиксил неправильный пересчет фасовок в накладной

This commit is contained in:
2025-12-27 09:24:21 +03:00
parent dfd855cb6e
commit c2d382cb6a
12 changed files with 461 additions and 144 deletions

View File

@@ -1,86 +1,233 @@
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { List, Typography, Tag, Spin, Empty } from 'antd';
import { useNavigate } from 'react-router-dom';
import { ArrowRightOutlined } from '@ant-design/icons';
import { api } from '../services/api';
// src/pages/DraftsList.tsx
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
List,
Typography,
Tag,
Spin,
Empty,
DatePicker,
Flex,
message,
} from "antd";
import { useNavigate } from "react-router-dom";
import {
ArrowRightOutlined,
ThunderboltFilled,
HistoryOutlined,
FileTextOutlined,
} from "@ant-design/icons";
import dayjs, { Dayjs } from "dayjs";
import { api } from "../services/api";
import type { UnifiedInvoice } from "../services/types";
const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
export const DraftsList: React.FC = () => {
const navigate = useNavigate();
const { data: drafts, isLoading, isError } = useQuery({
queryKey: ['drafts'],
queryFn: api.getDrafts,
refetchOnWindowFocus: true
// Состояние фильтра дат: по умолчанию последние 30 дней
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([
dayjs().subtract(30, "day"),
dayjs(),
]);
// Запрос данных с учетом дат (даты в ключе обеспечивают авто-перезапрос)
const {
data: invoices,
isLoading,
isError,
} = useQuery({
queryKey: [
"drafts",
dateRange[0].format("YYYY-MM-DD"),
dateRange[1].format("YYYY-MM-DD"),
],
queryFn: () =>
api.getDrafts(
dateRange[0].format("YYYY-MM-DD"),
dateRange[1].format("YYYY-MM-DD")
),
});
const getStatusTag = (status: string) => {
switch (status) {
case 'PROCESSING': return <Tag color="blue">Обработка</Tag>;
case 'READY_TO_VERIFY': return <Tag color="orange">Проверка</Tag>;
case 'COMPLETED': return <Tag color="green">Готово</Tag>;
case 'ERROR': return <Tag color="red">Ошибка</Tag>;
case 'CANCELED': return <Tag color="default" style={{ color: '#999' }}>Отменен</Tag>;
default: return <Tag>{status}</Tag>;
const getStatusTag = (item: UnifiedInvoice) => {
if (item.type === "SYNCED") {
return (
<Tag icon={<HistoryOutlined />} color="success">
Синхронизировано
</Tag>
);
}
switch (item.status) {
case "PROCESSING":
return <Tag color="blue">Обработка</Tag>;
case "READY_TO_VERIFY":
return <Tag color="orange">Проверка</Tag>;
case "COMPLETED":
return <Tag color="green">Готово</Tag>;
case "ERROR":
return <Tag color="red">Ошибка</Tag>;
case "CANCELED":
return <Tag color="default">Отменен</Tag>;
default:
return <Tag>{item.status}</Tag>;
}
};
if (isLoading) {
return <div style={{ textAlign: 'center', padding: 40 }}><Spin size="large" /></div>;
}
const handleInvoiceClick = (item: UnifiedInvoice) => {
if (item.type === "SYNCED") {
message.info("История доступна только для просмотра");
return;
}
navigate(`/invoice/${item.id}`);
};
if (isError) {
return <div style={{ padding: 20, textAlign: 'center' }}>Ошибка загрузки списка</div>;
return (
<div style={{ padding: 20 }}>
<Text type="danger">Ошибка загрузки списка накладных</Text>
</div>
);
}
return (
<div style={{ padding: '0 16px 20px' }}>
<Title level={4} style={{ marginTop: 16, marginBottom: 16 }}>Черновики накладных</Title>
{(!drafts || drafts.length === 0) ? (
<Empty description="Нет активных черновиков" />
<div style={{ padding: "0 4px 20px" }}>
<Title level={4} style={{ marginTop: 16, marginBottom: 16 }}>
Накладные
</Title>
{/* Фильтр дат */}
<div
style={{
marginBottom: 16,
background: "#fff",
padding: 12,
borderRadius: 8,
}}
>
<Text
type="secondary"
style={{ display: "block", marginBottom: 8, fontSize: 12 }}
>
Период загрузки:
</Text>
<RangePicker
value={dateRange}
onChange={(dates) => dates && setDateRange([dates[0]!, dates[1]!])}
style={{ width: "100%" }}
allowClear={false}
format="DD.MM.YYYY"
/>
</div>
{isLoading ? (
<div style={{ textAlign: "center", padding: 40 }}>
<Spin size="large" />
</div>
) : !invoices || invoices.length === 0 ? (
<Empty description="Нет данных за выбранный период" />
) : (
<List
itemLayout="horizontal"
dataSource={drafts}
renderItem={(item) => (
<List.Item
style={{
background: '#fff',
padding: 12,
marginBottom: 8,
borderRadius: 8,
cursor: 'pointer',
opacity: item.status === 'CANCELED' ? 0.6 : 1 // Делаем отмененные бледными
}}
onClick={() => navigate(`/invoice/${item.id}`)}
>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Text strong style={{ fontSize: 16, textDecoration: item.status === 'CANCELED' ? 'line-through' : 'none' }}>
{item.document_number || 'Без номера'}
dataSource={invoices}
renderItem={(item) => {
const isSynced = item.type === "SYNCED";
return (
<List.Item
style={{
background: isSynced ? "#fafafa" : "#fff",
padding: 12,
marginBottom: 10,
borderRadius: 12,
cursor: isSynced ? "default" : "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.is_app_created && (
<ThunderboltFilled
style={{ color: "#faad14" }}
title="Создано в RMSer"
/>
)}
</Flex>
{item.incoming_number && (
<Text type="secondary" style={{ fontSize: 12 }}>
Вх. {item.incoming_number}
</Text>
)}
</Flex>
{getStatusTag(item)}
</Flex>
<Flex justify="space-between" style={{ marginTop: 4 }}>
<Flex gap={8} align="center">
<FileTextOutlined style={{ color: "#888" }} />
<Text type="secondary" style={{ fontSize: 13 }}>
{item.items_count} поз.
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
{dayjs(item.date_incoming).format("DD.MM.YYYY")}
</Text>
</Flex>
{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>
{getStatusTag(item.status)}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#888', fontSize: 13 }}>
<span>{new Date(item.date_incoming).toLocaleDateString()}</span>
<span>{item.items_count} поз.</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 6, alignItems: 'center' }}>
<Text strong>
{item.total_sum.toLocaleString('ru-RU', { style: 'currency', currency: 'RUB' })}
</Text>
<ArrowRightOutlined style={{ color: '#1890ff' }} />
</div>
</div>
</List.Item>
)}
{!isSynced && (
<ArrowRightOutlined style={{ color: "#1890ff" }} />
)}
</Flex>
</Flex>
</List.Item>
);
}}
/>
)}
</div>
);
};
};

View File

@@ -22,7 +22,7 @@ import type {
AddContainerRequest,
AddContainerResponse,
DictionariesResponse,
DraftSummary,
UnifiedInvoice,
ServerUser,
UserRole
} from './types';
@@ -159,8 +159,11 @@ export const api = {
return data.suppliers;
},
getDrafts: async (): Promise<DraftSummary[]> => {
const { data } = await apiClient.get<DraftSummary[]>('/drafts');
// Обновленный метод получения списка накладных с фильтрацией
getDrafts: async (from?: string, to?: string): Promise<UnifiedInvoice[]> => {
const { data } = await apiClient.get<UnifiedInvoice[]>('/drafts', {
params: { from, to }
});
return data;
},

View File

@@ -233,4 +233,20 @@ export interface MainUnit {
id: UUID;
name: string; // "кг"
code: string;
}
export type InvoiceType = 'DRAFT' | 'SYNCED'; // Тип записи: Черновик или Синхронизировано из iiko
export interface UnifiedInvoice {
id: UUID;
type: InvoiceType; // Новый признак типа
document_number: string; // Внутренний номер iiko или ID черновика
incoming_number: string; // Входящий номер накладной от поставщика
date_incoming: string;
status: DraftStatus;
items_count: number;
total_sum: number;
store_name?: string;
created_at: string;
is_app_created: boolean; // Создано ли через наше приложение
}