USER
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { RootState } from './store';
interface DashboardState {
apiKey: string;
period: string;
sales: number;
costPrice: number;
returnsCancellations: number;
additionalServices: number;
compensations: number;
commission: number;
logisticsCosts: number;
marginProfit: number;
stockWB: number;
stockSeller: number;
statistics: {
ordered: number;
revenue: number;
cancelled: number;
returned: number;
};
expenses: {
tax?: number;
logistics?: number;
packaging?: number;
other?: number;
};
data: any[];
}
const initialState: DashboardState = {
apiKey: '',
period: 'week',
sales: 0,
costPrice: 0,
returnsCancellations: 0,
additionalServices: 0,
compensations: 0,
commission: 0,
logisticsCosts: 0,
marginProfit: 0,
stockWB: 0,
stockSeller: 0,
statistics: {
ordered: 0,
revenue: 0,
cancelled: 0,
returned: 0,
},
expenses: {},
data: [],
};
interface FetchDashboardDataPayload {
sales: number;
returnsCancellations: number;
additionalServices: number;
compensations: number;
commission: number;
logisticsCosts: number;
marginProfit: number;
stockWB: number;
stockSeller: number;
statistics: DashboardState['statistics'];
data: any[];
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const formatDate = (date: Date) => date.toISOString().split('T')[0];
const createAxiosInstance = (apiKey: string) => {
return axios.create({
headers: { Authorization: apiKey },
});
};
const fetchData = async (
axiosInstance: ReturnType<typeof createAxiosInstance>,
dateFrom: string,
dateTo: string,
) => {
const requests = [
axiosInstance.get(`https://statistics-api.wildberries.ru/api/v1/supplier/sales`, {
params: { dateFrom, dateTo, flag: 0 },
}),
axiosInstance.get(`https://statistics-api.wildberries.ru/api/v1/supplier/orders`, {
params: { dateFrom, dateTo, flag: 1 },
}),
axiosInstance.get(`https://statistics-api.wildberries.ru/api/v1/supplier/stocks`, {
params: { dateFrom, dateTo },
}),
axiosInstance.get(`https://statistics-api.wildberries.ru/api/v1/supplier/incomes`, {
params: { dateFrom, dateTo },
}),
axiosInstance.get(
`https://statistics-api.wildberries.ru/api/v5/supplier/reportDetailByPeriod`,
{
params: { dateFrom, dateTo, limit: 100000, rrdid: 0 },
},
),
];
return Promise.all(requests);
};
export const fetchDashboardData = createAsyncThunk<
FetchDashboardDataPayload,
void,
{ state: RootState }
>('dashboard/fetchDashboardData', async (_, { getState, rejectWithValue }) => {
const state = getState();
const { apiKey } = state.dashboard;
if (!apiKey) {
return rejectWithValue('API key is missing');
}
const dateTo = new Date();
const dateFrom = new Date(dateTo);
dateFrom.setMonth(dateTo.getMonth() - 3);
const dateFromStr = formatDate(dateFrom);
const dateToStr = formatDate(dateTo);
const axiosInstance = createAxiosInstance(apiKey);
let retryCount = 0;
const maxRetries = 5;
let retryDelay = 1000;
while (retryCount < maxRetries) {
try {
const [
salesResponse,
returnsCancellationsResponse,
additionalServicesResponse,
compensationsResponse,
commissionResponse,
] = await fetchData(axiosInstance, dateFromStr, dateToStr);
const totalSalesAmount = salesResponse.data
.filter((item: any) => item.saleID.startsWith('S'))
.reduce((sum: number, item: any) => sum + item.forPay, 0);
return {
sales: totalSalesAmount,
returnsCancellations: returnsCancellationsResponse.data.length,
additionalServices: additionalServicesResponse.data.length,
compensations: compensationsResponse.data.length,
commission: commissionResponse.data.length,
logisticsCosts: 0, // Assuming logisticsCosts is not provided in the responses
marginProfit: 0, // Assuming marginProfit is not provided in the responses
stockWB: 0, // Assuming stockWB is not provided in the responses
stockSeller: 0, // Assuming stockSeller is not provided in the responses
statistics: {
ordered: 0, // Assuming ordered is not provided in the responses
revenue: totalSalesAmount,
cancelled: 0, // Assuming cancelled is not provided in the responses
returned: 0, // Assuming returned is not provided in the responses
},
data: salesResponse.data,
};
} catch (error: any) {
if (error.response && error.response.status === 429) {
retryCount++;
console.error(`Rate limited. Retrying ${retryCount}/${maxRetries}...`);
await delay(retryDelay);
retryDelay *= 2;
} else {
console.error('Error fetching dashboard data:', error);
return rejectWithValue(error.response?.data || error.message || 'Unknown error');
}
}
}
return rejectWithValue('Max retries exceeded');
});
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setApiKey: (state, action: PayloadAction<string>) => {
state.apiKey = action.payload;
},
setPeriod: (state, action: PayloadAction<string>) => {
state.period = action.payload;
},
setSales: (state, action: PayloadAction<number>) => {
state.sales = action.payload;
},
setCostPrice: (state, action: PayloadAction<number>) => {
state.costPrice = action.payload;
},
setReturnsCancellations: (state, action: PayloadAction<number>) => {
state.returnsCancellations = action.payload;
},
setAdditionalServices: (state, action: PayloadAction<number>) => {
state.additionalServices = action.payload;
},
setCompensations: (state, action: PayloadAction<number>) => {
state.compensations = action.payload;
},
setCommission: (state, action: PayloadAction<number>) => {
state.commission = action.payload;
},
setLogisticsCosts: (state, action: PayloadAction<number>) => {
state.logisticsCosts = action.payload;
},
setMarginProfit: (state, action: PayloadAction<number>) => {
state.marginProfit = action.payload;
},
setStockWB: (state, action: PayloadAction<number>) => {
state.stockWB = action.payload;
},
setStockSeller: (state, action: PayloadAction<number>) => {
state.stockSeller = action.payload;
},
setStatistics: (state, action: PayloadAction<DashboardState['statistics']>) => {
state.statistics = action.payload;
},
setExpenses: (state, action: PayloadAction<DashboardState['expenses']>) => {
state.expenses = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchDashboardData.pending, (state) => {
// Handle loading state if needed
})
.addCase(fetchDashboardData.fulfilled, (state, action) => {
state.sales = action.payload.sales;
state.returnsCancellations = action.payload.returnsCancellations;
state.additionalServices = action.payload.additionalServices;
state.compensations = action.payload.compensations;
state.commission = action.payload.commission;
state.logisticsCosts = action.payload.logisticsCosts;
state.marginProfit = action.payload.marginProfit;
state.stockWB = action.payload.stockWB;
state.stockSeller = action.payload.stockSeller;
state.statistics = action.payload.statistics;
state.data = action.payload.data;
})
.addCase(fetchDashboardData.rejected, (state, action) => {
console.error('Error fetching dashboard data:', action.payload);
});
},
});
export const {
setApiKey,
setPeriod,
setSales,
setCostPrice,
setReturnsCancellations,
setAdditionalServices,
setCompensations,
setCommission,
setLogisticsCosts,
setMarginProfit,
setStockWB,
setStockSeller,
setStatistics,
setExpenses,
} = dashboardSlice.actions;
export default dashboardSlice.reducer;
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';
import { setApiKey, setPeriod, fetchDashboardData } from '../redux/dashboardSlice';
import Modal from './Modal';
import Sales from './Sales';
import CostPrice from './CostPrice';
import ReturnsCancellations from './ReturnsCancellations';
import AdditionalServices from './AdditionalServices';
import Compensations from './Compensations';
import Commission from './Commission';
import LogisticsCosts from './LogisticsCosts';
import MarginProfit from './MarginProfit';
import StockWB from './StockWB';
import StockSeller from './StockSeller';
import Statistics from './Statistics';
import ExpensesModal from './ExpensesModal';
import '../scss/Dashboard.scss';
const Dashboard: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const { apiKey, period, data } = useSelector((state: RootState) => state.dashboard);
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const [isExpensesModalOpen, setIsExpensesModalOpen] = useState(false);
const [localApiKey, setLocalApiKey] = useState(apiKey);
useEffect(() => {
if (apiKey) {
dispatch(fetchDashboardData());
}
}, [apiKey, dispatch]);
const handleApiKeyChange = () => {
dispatch(setApiKey(localApiKey));
dispatch(fetchDashboardData()); // Fetch data immediately after setting the API key
setIsApiKeyModalOpen(false);
};
const handlePeriodChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
dispatch(setPeriod(event.target.value));
};
const getFilteredData = (period: string, data: any[]) => {
const dateTo = new Date();
const dateFrom = new Date(dateTo);
switch (period) {
case 'week':
dateFrom.setDate(dateTo.getDate() - 7);
break;
case 'month':
dateFrom.setMonth(dateTo.getMonth() - 1);
break;
case 'quarter':
dateFrom.setMonth(dateTo.getMonth() - 3);
break;
default:
dateFrom.setDate(dateTo.getDate() - 7);
break;
}
const formatDate = (date: Date) => date.toISOString().split('T')[0];
const dateFromStr = formatDate(dateFrom);
const dateToStr = formatDate(dateTo);
// console.log('Filtering data from:', dateFromStr, 'to:', dateToStr);
const filtered = data.filter((item: any) => {
const itemDate = new Date(item.date);
const itemDateStr = itemDate.toISOString().split('T')[0];
// console.log('Item date:', itemDateStr, 'Item:', item);
return itemDateStr >= dateFromStr && itemDateStr <= dateToStr;
});
// console.log('First filtered item:', filtered[0]);
// console.log('Last filtered item:', filtered[filtered.length - 1]);
return filtered;
};
const filteredData = getFilteredData(period, [...data]); // Create a copy of the data array
// console.log('Filtered data count:', filteredData.length);
// console.log('Original data count:', data.length);
// console.log('First original item:', data[0]);
// console.log('Last original item:', data[data.length - 1]);
return (
<div className="dashboard">
<div className="dashboard-header">
<select value={period} onChange={handlePeriodChange}>
<option value="week">Неделя</option>
<option value="month">Месяц</option>
<option value="quarter">Квартал</option>
</select>
<button onClick={() => setIsApiKeyModalOpen(true)}>Ввести API ключ</button>
</div>
<div className="dashboard-grid">
<Sales data={filteredData} />
<CostPrice data={filteredData} />
<ReturnsCancellations data={filteredData} />
<AdditionalServices data={filteredData} />
<Compensations data={filteredData} />
<Commission data={filteredData} />
<LogisticsCosts data={filteredData} />
<MarginProfit data={filteredData} />
<StockWB data={filteredData} />
<StockSeller data={filteredData} />
<Statistics data={filteredData} />
<button onClick={() => setIsExpensesModalOpen(true)}>Учесть расходы</button>
</div>
{isApiKeyModalOpen && (
<Modal title="Введите API ключ" onClose={() => setIsApiKeyModalOpen(false)}>
<input
type="text"
value={localApiKey}
onChange={(e) => setLocalApiKey(e.target.value)}
placeholder="API ключ"
/>
<button onClick={handleApiKeyChange}>Сохранить</button>
</Modal>
)}
{isExpensesModalOpen && <ExpensesModal onClose={() => setIsExpensesModalOpen(false)} />}
</div>
);
};
export default Dashboard;
import React from 'react';
interface StatisticsProps {
data: any[];
}
const Statistics: React.FC<StatisticsProps> = ({ data }) => {
const ordered = data.filter((item: any) => item.saleID.startsWith('S')).length;
const revenue = data
.filter((item: any) => item.saleID.startsWith('S'))
.reduce((sum: number, item: any) => sum + item.forPay, 0);
const cancelled = data.filter((item: any) => item.isCancel).length;
const returned = data.filter((item: any) => item.saleID.startsWith('R')).length;
return (
<div className="dashboard-item">
<h3>Статистика</h3>
<p>Заказано товаров: {ordered} шт.</p>
<p>Выручка: {revenue} ₽</p>
<p>Отменено товаров: {cancelled} шт.</p>
<p>Возвращено товаров: {returned} шт.</p>
</div>
);
};
export default Statistics;
в данном коде <p>Заказано товаров: {ordered} шт.</p> выводиться не те данные , они точно такие же что и продажа товара, нужно выяснить где ошибка, заменить на правильный код, потому что заказанный товар и купленный не может быть одинаковым количеством потому что часть заказов всегда отменяется ASSISTANT
Давайте разберемся. Судя по вашему описанию и коду, проблема в том, что метрика "Заказано товаров (`ordered`)" выводится неправильно и совпадает с количеством "Продано товаров" (т.е. продаж). Этот результат происходит из-за применения `item.saleID.startsWith('S')` для вычисления `ordered` и выгружаемых данных. Однако "Заказы товаров" (`orders`) и "Продажи товаров" (`sales`) — это два разных понятия, которые нужно обработать корректно.
#### Причина проблемы:
Ваша логика для вычисления `ordered` и для расчета `revenue` опирается на один и тот же источник данных (`saleID` из продаж), а для заказов вам нужно использовать данные запроса `/api/v1/supplier/orders`. Именно в этих данных содержится информация о заказах. Это вызвано тем, что вы смешали логику заказов и продаж. Запросы для заказов и продаж имеют разные API endpoints.
---
#### Шаги исправления:
1. Убедимся, что данные о заказах (`ordered`) подгружаются из API `/api/v1/supplier/orders`. Там вы получите корректный список заказов.
2. Добавьте в логику `Statistics` отдельную обработку количества заказов с использованием данных, возвращенных вашим запросом к `/api/v1/supplier/orders`.
---
### Решение:
**1. Внесение изменений в часть Redux Toolkit (Action и Thunk)**
В `fetchDashboardData` добавьте обработку количества заказов (`ordered`) с `ordersResponse.data.length`:
```ts
const [
salesResponse,
ordersResponse, // Получаем данные заказов
additionalServicesResponse,
compensationsResponse,
commissionResponse,
] = await fetchData(axiosInstance, dateFromStr, dateToStr);
const totalOrdersAmount = ordersResponse.data.filter((item: any) => item.orderType === "Клиентский").length; // Фильтруем только клиентские заказы
const cancelledOrders = ordersResponse.data.filter((item: any) => item.isCancel).length;
const totalSalesAmount = salesResponse.data
.filter((item: any) => item.saleID.startsWith('S'))
.reduce((sum: number, item: any) => sum + item.forPay, 0);
return {
sales: totalSalesAmount,
returnsCancellations: returnsCancellationsResponse.data.length,
additionalServices: additionalServicesResponse.data.length,
compensations: compensationsResponse.data.length,
commission: commissionResponse.data.length,
logisticsCosts: 0,
marginProfit: 0,
stockWB: 0,
stockSeller: 0,
statistics: {
ordered: totalOrdersAmount,
revenue: totalSalesAmount,
cancelled: cancelledOrders,
returned: salesResponse.data.filter((item: any) => item.saleID.startsWith('R')).length,
},
data: salesResponse.data,
};
```
---
**2. Измените компонент `Statistics` так, чтобы он использовал данные `ordered` и `cancelled` из глобального состояния**
Не фильтруйте проданные товары как количество заказанных — это разные данные.
Изменим `Statistics` следующим образом:
```tsx
import React from 'react';
interface StatisticsProps {
ordered: number;
revenue: number;
cancelled: number;
returned: number;
}
const Statistics: React.FC<StatisticsProps> = ({ ordered, revenue, cancelled, returned }) => (
<div className="dashboard-item">
<h3>Статистика</h3>
<p>Заказано товаров: {ordered} шт.</p>
<p>Выручка: {revenue} ₽</p>
<p>Отменено заказов: {cancelled} шт.</p>
<p>Возвращено товаров: {returned} шт.</p>
</div>
);
export default Statistics;
```
---
**3. Передавайте корректные данные в компонент `Statistics` из Dashboard**
Адаптируйте логику компонента `Dashboard`, чтобы данные из глобального состояния передавались корректно:
```tsx
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';
import { fetchDashboardData } from '../redux/dashboardSlice';
import Statistics from './Statistics';
const Dashboard: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
const { apiKey, period, statistics } = useSelector((state: RootState) => state.dashboard);
useEffect(() => {
if (apiKey) {
dispatch(fetchDashboardData());
}
}, [apiKey, dispatch]);
return (
<div className="dashboard">
{/* ... */}
<div className="dashboard-grid">
<Statistics
ordered={statistics.ordered}
revenue={statistics.revenue}
cancelled={statistics.cancelled}
returned={statistics.returned}
/>
</div>
</div>
);
};
export default Dashboard;
```
---
**4. Вывод данных "количество заказов" (`ordered`) + "отменено заказов" (`cancelled`)**
Теперь компонент `Statistics` будет корректно отображать количество заказов на основании вызова `/api/v1/supplier/orders`. Отмененные заказы отображаются отдельно.
---
### Объяснение изменений:
- `ordered` (`Количество заказов`) теперь рассчитывается через API Wildberries `/api/v1/supplier/orders`, а не через данные о продажах (`sales`).
- Логика отмененных заказов (`cancelled`) взята из API Wildberries `/api/v1/supplier/orders`.
- Корректно разделены понятия "продажи" (со статусом `sale`) и "заказы" на уровне компонента.
### Ожидаемый результат:
Теперь метрика `Заказано товаров` и `Продано товаров` будут разными, так как учитывается разница между количеством заказов и фактическими продажами. Разделение "заказов" и "продаж" устранит текущую ошибку дублирования.