mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
пофиксил неправильный пересчет фасовок в накладной
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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; // Создано ли через наше приложение
|
||||
}
|
||||
Reference in New Issue
Block a user