diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..03ce071
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+# Зависимости
+/node_modules
+/.pnp
+.pnp.js
+
+# Тестирование
+/coverage
+
+# Сборка
+/dist
+/build
+
+# Env файлы
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Логи
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IDE
+.idea
+.vscode
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d24fdb2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:18-alpine
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm install
+
+COPY . .
+
+EXPOSE 3000
+
+CMD ["npm", "run", "dev"]
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..2a41001
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,21 @@
+version: '3.8'
+
+services:
+ frontend:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "3000:3000"
+ volumes:
+ - .:/app
+ - /app/node_modules
+ environment:
+ - NODE_ENV=development
+ - VITE_API_URL=http://localhost:4000
+ networks:
+ - warehouse-network
+
+networks:
+ warehouse-network:
+ name: warehouse-network
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..000b55a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ Warehouse Management
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4b643d4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "warehouse-management-frontend",
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "@ant-design/icons": "^5.2.6",
+ "antd": "^5.11.1",
+ "axios": "^1.6.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.28.2"
+ },
+ "devDependencies": {
+ "@types/node": "^20.9.0",
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@vitejs/plugin-react": "^4.1.1",
+ "typescript": "^5.2.2",
+ "vite": "^4.5.0",
+ "vite-plugin-utils": "^0.4.5"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..11f01ea
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..faf90fb
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { RouterProvider } from 'react-router-dom';
+import { ConfigProvider, App as AntdApp } from 'antd';
+import ruRU from 'antd/locale/ru_RU';
+import router from './router';
+
+const App: React.FC = () => {
+ return (
+
+ {/* Важно: оборачиваем в AntdApp */}
+
+
+
+ );
+};
+
+export default App;
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
new file mode 100644
index 0000000..7f74ff5
--- /dev/null
+++ b/src/components/Dashboard.tsx
@@ -0,0 +1,152 @@
+import React, { useState, useEffect } from 'react';
+import { Layout, Menu, MenuProps, theme } from 'antd';
+import { Routes, Route, Link, useNavigate } from 'react-router-dom';
+import {
+ MenuFoldOutlined,
+ MenuUnfoldOutlined,
+ InboxOutlined,
+ FileOutlined,
+ UserOutlined,
+ SettingOutlined,
+ LogoutOutlined,
+ EnvironmentOutlined,
+} from '@ant-design/icons';
+import CategoryManagement from './admin/CategoryManagement';
+import UserManagement from './admin/UserManagement';
+import RoleManagement from './admin/RoleManagement';
+import ReportGenerator from './reports/ReportGenerator';
+import CategoryItems from './inventory/CategoryItems';
+import PrivateRoute from './PrivateRoute';
+import LocationManagement from './admin/LocationManagement';
+
+const { Header, Sider, Content } = Layout;
+
+const Dashboard: React.FC = () => {
+ const [collapsed, setCollapsed] = useState(false);
+ const { token: { colorBgContainer } } = theme.useToken();
+ const [userRole, setUserRole] = useState(null);
+ const navigate = useNavigate();
+
+ // ✅ Получаем роль пользователя через локальное хранилище
+ useEffect(() => {
+ const user = localStorage.getItem('user');
+ const parsedUser = user ? JSON.parse(user) : null;
+ setUserRole(parsedUser?.role || null);
+ }, []);
+
+ const handleLogout = () => {
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ navigate('/login');
+ };
+
+ const menuItems: MenuProps['items'] = [
+ {
+ key: '1',
+ icon: ,
+ label: Инвентарь,
+ },
+ {
+ key: '2',
+ icon: ,
+ label: Отчеты,
+ },
+ ];
+
+ // ✅ Добавляем "Администрирование", если роль admin
+ if (userRole === 'admin') {
+ menuItems.push({
+ key: 'admin',
+ icon: ,
+ label: collapsed ? : 'Управление', // ✅ Фикс стрелки
+ children: [
+ {
+ key: '3',
+ icon: ,
+ label: Пользователи,
+ },
+ {
+ key: '4',
+ icon: ,
+ label: Роли,
+ },
+ {
+ key: '5',
+ icon: ,
+ label: Категории,
+ },
+ {
+ key: '6',
+ icon: ,
+ label: Локации,
+ },
+ ],
+ });
+ }
+
+ // ✅ Добавляем кнопку "Выйти" в `menuItems`, чтобы она скрывалась при сворачивании
+ menuItems.push({
+ key: 'logout',
+ icon: ,
+ label: Выйти,
+ onClick: handleLogout, // ✅ Добавляем обработчик выхода
+ });
+
+ return (
+
+
+
+
+
+
+
+ {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
+ className: 'trigger',
+ onClick: () => setCollapsed(!collapsed),
+ style: { fontSize: '18px', padding: '0 24px', cursor: 'pointer' },
+ })}
+
+
+
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/src/components/ItemsList.tsx b/src/components/ItemsList.tsx
new file mode 100644
index 0000000..4759e7f
--- /dev/null
+++ b/src/components/ItemsList.tsx
@@ -0,0 +1,89 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Input, Button, Space } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+
+interface Item {
+ id: number;
+ name: string;
+ code: string;
+ quantity: number;
+ category: string;
+}
+
+const ItemsList: React.FC = () => {
+ const [items, setItems] = useState- ([]);
+ const [loading, setLoading] = useState(false);
+ const [searchText, setSearchText] = useState('');
+
+ useEffect(() => {
+ fetchItems();
+ }, []);
+
+ const fetchItems = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch('/api/items');
+ const data = await response.json();
+ setItems(data);
+ } catch (error) {
+ console.error('Ошибка при загрузке товаров:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const columns = [
+ {
+ title: 'Название',
+ dataIndex: 'name',
+ key: 'name',
+ sorter: (a: Item, b: Item) => a.name.localeCompare(b.name),
+ },
+ {
+ title: 'Код',
+ dataIndex: 'code',
+ key: 'code',
+ },
+ {
+ title: 'Количество',
+ dataIndex: 'quantity',
+ key: 'quantity',
+ sorter: (a: Item, b: Item) => a.quantity - b.quantity,
+ },
+ {
+ title: 'Категория',
+ dataIndex: 'category',
+ key: 'category',
+ filters: [
+ { text: 'Запчасти', value: 'Запчасти' },
+ { text: 'Топливо', value: 'Топливо' },
+ { text: 'Масла', value: 'Масла' },
+ ],
+ },
+ ];
+
+ return (
+
+
+ }
+ value={searchText}
+ onChange={e => setSearchText(e.target.value)}
+ />
+
+
+
+
+ );
+};
+
+export default ItemsList;
\ No newline at end of file
diff --git a/src/components/Login.tsx b/src/components/Login.tsx
new file mode 100644
index 0000000..a759354
--- /dev/null
+++ b/src/components/Login.tsx
@@ -0,0 +1,91 @@
+import React, { useState } from 'react';
+import { Form, Input, Button, App as AntdApp } from 'antd';
+import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+
+interface LoginForm {
+ username: string;
+ password: string;
+}
+
+const Login: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const { message } = AntdApp.useApp();
+ const onFinish = async (values: LoginForm) => {
+ setLoading(true);
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(values),
+ });
+
+ if (!response.ok) {
+ throw new Error('Ошибка авторизации');
+ }
+
+ const data = await response.json();
+ localStorage.setItem('token', data.token);
+ localStorage.setItem('user', JSON.stringify(data.user));
+
+ message.success('Успешная авторизация');
+ console.log("Успешная авторизация");
+ navigate('/dashboard');
+ } catch (error) {
+ message.error('Неверное имя пользователя или пароль');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
Вход в систему
+
+ }
+ placeholder="Имя пользователя"
+ size="large"
+ />
+
+
+
+ }
+ placeholder="Пароль"
+ size="large"
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/src/components/PrivateRoute.tsx b/src/components/PrivateRoute.tsx
new file mode 100644
index 0000000..631984a
--- /dev/null
+++ b/src/components/PrivateRoute.tsx
@@ -0,0 +1,86 @@
+import React, { useEffect, useState } from 'react';
+import { Navigate, useLocation } from 'react-router-dom';
+import { Spin } from 'antd';
+import { useApi } from '../hooks/useApi';
+
+interface PrivateRouteProps {
+ children: React.ReactNode;
+ requiredRoles?: string[]; // Теперь соответствует router.tsx
+}
+
+interface User {
+ id: number;
+ username: string;
+ role: string;
+}
+
+const PrivateRoute: React.FC = ({ children, requiredRoles }) => {
+ const [isAuthenticated, setIsAuthenticated] = useState(null);
+ const [user, setUser] = useState(null);
+ const location = useLocation();
+ const { execute: validateToken } = useApi<{ valid: boolean; user: User }>('/auth/validate', 'POST');
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const checkAuth = async () => {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ if (isMounted) setIsAuthenticated(false);
+ return;
+ }
+
+ try {
+ const response = await validateToken({});
+ if (response?.valid) {
+ if (isMounted) {
+ setIsAuthenticated(true);
+ setUser(response.user);
+ }
+ } else {
+ if (isMounted) {
+ setIsAuthenticated(false);
+ localStorage.removeItem('token');
+ }
+ }
+ } catch {
+ if (isMounted) {
+ setIsAuthenticated(false);
+ localStorage.removeItem('token');
+ }
+ }
+ };
+
+ checkAuth();
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ // Показываем спиннер во время проверки
+ if (isAuthenticated === null) {
+ return (
+
+
+ {/* Пустой div как вложенный контент */}
+
+
+ );
+ }
+
+
+ // Если не авторизован, редиректим на логин
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // Проверяем роль, если требуется
+ if (requiredRoles && user && !requiredRoles.includes(user.role)) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
+export default PrivateRoute;
diff --git a/src/components/admin/AdminPanel.tsx b/src/components/admin/AdminPanel.tsx
new file mode 100644
index 0000000..ea17bd3
--- /dev/null
+++ b/src/components/admin/AdminPanel.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { Layout, Menu } from 'antd';
+import { UserOutlined, TeamOutlined, AppstoreOutlined } from '@ant-design/icons';
+import { Routes, Route, useNavigate } from 'react-router-dom';
+import UserManagement from './UserManagement';
+import RoleManagement from './RoleManagement';
+import CategoryManagement from './CategoryManagement';
+
+const { Content, Sider } = Layout;
+
+const AdminPanel: React.FC = () => {
+ const navigate = useNavigate();
+
+ const menuItems = [
+ {
+ key: 'users',
+ icon: ,
+ label: 'Пользователи',
+ onClick: () => navigate('/admin/users')
+ },
+ {
+ key: 'roles',
+ icon: ,
+ label: 'Роли и разрешения',
+ onClick: () => navigate('/admin/roles')
+ },
+ {
+ key: 'categories',
+ icon: ,
+ label: 'Категории',
+ onClick: () => navigate('/admin/categories')
+ }
+ ];
+
+ return (
+
+
+
+
+
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ );
+};
+
+export default AdminPanel;
\ No newline at end of file
diff --git a/src/components/admin/CategoryManagement.tsx b/src/components/admin/CategoryManagement.tsx
new file mode 100644
index 0000000..f2c5822
--- /dev/null
+++ b/src/components/admin/CategoryManagement.tsx
@@ -0,0 +1,178 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, Select, Space, App as AntdApp } from 'antd';
+import { useGet, usePost, usePut, useDelete } from '../../hooks/useApi';
+
+interface Location {
+ id: number;
+ name: string;
+}
+
+interface Category {
+ id: number;
+ name: string;
+ location_id: number;
+ location: Location;
+}
+
+const CategoryManagement: React.FC = () => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editingCategory, setEditingCategory] = useState(null);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для загрузки категорий и локаций
+ const { data: categories, execute: fetchCategories } = useGet('/admin/categories');
+ const { data: locations, execute: fetchLocations } = useGet('/admin/locations');
+
+ // Используем `usePost`, `usePut` и `useDelete` для работы с категориями
+ const { execute: createCategory } = usePost('/admin/categories');
+ const { execute: updateCategory } = usePut(`/admin/categories/${editingCategory?.id}`);
+ const { execute: deleteCategory } = useDelete(`/admin/categories`);
+
+ // Загружаем данные при монтировании компонента
+ useEffect(() => {
+ fetchCategories();
+ fetchLocations();
+ }, []);
+
+ // Отправка формы (создание или обновление категории)
+ const handleSubmit = async (values: any) => {
+ setLoading(true);
+ try {
+ if (editingCategory) {
+ await updateCategory(values);
+ message.success('Категория обновлена');
+ } else {
+ await createCategory(values);
+ message.success('Категория создана');
+ }
+ setIsModalVisible(false);
+ form.resetFields();
+ setEditingCategory(null);
+ fetchCategories(); // Обновляем список категорий
+ } catch {
+ message.error('Ошибка при сохранении категории');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Открытие модального окна для редактирования категории
+ const handleEdit = (category: Category) => {
+ setEditingCategory(category);
+ form.setFieldsValue({
+ name: category.name,
+ location_id: category.location_id
+ });
+ setIsModalVisible(true);
+ };
+
+ // Удаление категории
+ const handleDelete = async (categoryId: number) => {
+ try {
+ await deleteCategory(categoryId);
+ message.success('Категория удалена');
+ fetchCategories(); // Обновляем список категорий
+ } catch {
+ message.error('Ошибка при удалении категории');
+ }
+ };
+
+ // Определение столбцов таблицы
+ const columns = [
+ {
+ title: 'Название',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: 'Локация',
+ dataIndex: ['location', 'name'],
+ key: 'location',
+ },
+ {
+ title: 'Действия',
+ key: 'actions',
+ render: (_: any, record: Category) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
form.submit()}
+ onCancel={() => {
+ setIsModalVisible(false);
+ setEditingCategory(null);
+ form.resetFields();
+ }}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CategoryManagement;
diff --git a/src/components/admin/LocationManagement.tsx b/src/components/admin/LocationManagement.tsx
new file mode 100644
index 0000000..0138ebb
--- /dev/null
+++ b/src/components/admin/LocationManagement.tsx
@@ -0,0 +1,152 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, Space, App as AntdApp } from 'antd';
+import { useGet, usePost, usePut, useDelete } from '../../hooks/useApi';
+
+interface Location {
+ id: number;
+ name: string;
+ description?: string;
+}
+
+const LocationManagement: React.FC = () => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editingLocation, setEditingLocation] = useState(null);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+
+ // API хуки
+ const { data: locations, execute: fetchLocations } = useGet('/admin/locations');
+ const { execute: createLocation } = usePost('/admin/locations');
+ const { execute: updateLocation } = usePut(`/admin/locations/${editingLocation?.id}`);
+ const { execute: deleteLocation } = useDelete(`/admin/locations`);
+
+ useEffect(() => {
+ fetchLocations();
+ }, []);
+
+ const handleSubmit = async (values: any) => {
+ setLoading(true);
+ try {
+ if (editingLocation) {
+ await updateLocation(values);
+ message.success('Локация обновлена');
+ } else {
+ await createLocation(values);
+ message.success('Локация создана');
+ }
+ setIsModalVisible(false);
+ form.resetFields();
+ setEditingLocation(null);
+ fetchLocations();
+ } catch {
+ message.error('Ошибка при сохранении локации');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEdit = (location: Location) => {
+ setEditingLocation(location);
+ form.setFieldsValue({
+ name: location.name,
+ description: location.description
+ });
+ setIsModalVisible(true);
+ };
+
+ const handleDelete = async (locationId: number) => {
+ try {
+ await deleteLocation(locationId);
+ message.success('Локация удалена');
+ fetchLocations();
+ } catch {
+ message.error('Ошибка при удалении локации');
+ }
+ };
+
+ const columns = [
+ {
+ title: 'Название',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: 'Описание',
+ dataIndex: 'description',
+ key: 'description',
+ },
+ {
+ title: 'Действия',
+ key: 'actions',
+ render: (_: any, record: Location) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
form.submit()}
+ onCancel={() => {
+ setIsModalVisible(false);
+ setEditingLocation(null);
+ form.resetFields();
+ }}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LocationManagement;
\ No newline at end of file
diff --git a/src/components/admin/RoleManagement.tsx b/src/components/admin/RoleManagement.tsx
new file mode 100644
index 0000000..6ffac62
--- /dev/null
+++ b/src/components/admin/RoleManagement.tsx
@@ -0,0 +1,197 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, Checkbox, Space, App as AntdApp } from 'antd';
+import { useGet, usePost, usePut, useDelete } from '../../hooks/useApi';
+
+interface Permission {
+ id: number;
+ name: string;
+ description: string;
+}
+
+interface Role {
+ id: number;
+ name: string;
+ description: string;
+ permissions: Permission[];
+}
+
+const RoleManagement: React.FC = () => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editingRole, setEditingRole] = useState(null);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для загрузки ролей и разрешений
+ const { data: roles, execute: fetchRoles } = useGet('/admin/roles');
+ const { data: permissions, execute: fetchPermissions } = useGet('/admin/permissions');
+
+ // Используем `usePost`, `usePut` и `useDelete` для работы с ролями
+ const { execute: createRole } = usePost('/admin/roles');
+ const { execute: updateRole } = usePut(`/admin/roles/${editingRole?.id}`);
+ const { execute: deleteRole } = useDelete(`/admin/roles`);
+
+ // Загружаем данные при монтировании компонента
+ useEffect(() => {
+ fetchRoles();
+ fetchPermissions();
+ }, []);
+
+ // Открытие модального окна для редактирования роли
+ const handleEdit = (role: Role) => {
+ setEditingRole(role);
+ form.setFieldsValue({
+ name: role.name,
+ description: role.description,
+ permissions: role.permissions.map(p => p.id),
+ });
+ setIsModalVisible(true);
+ };
+
+ // Отправка формы (создание или обновление роли)
+ const handleSubmit = async (values: any) => {
+ setLoading(true);
+ try {
+ if (editingRole) {
+ await updateRole(values);
+ message.success('Роль обновлена');
+ } else {
+ await createRole(values);
+ message.success('Роль создана');
+ }
+ setIsModalVisible(false);
+ form.resetFields();
+ setEditingRole(null);
+ fetchRoles(); // Обновляем список ролей
+ } catch {
+ message.error('Ошибка при сохранении роли');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Удаление роли
+ const handleDelete = async (roleId: number) => {
+ try {
+ await deleteRole(roleId);
+ message.success('Роль удалена');
+ fetchRoles(); // Обновляем список ролей
+ } catch {
+ message.error('Ошибка при удалении роли');
+ }
+ };
+
+ // Определение столбцов таблицы
+ const columns = [
+ {
+ title: 'Название',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: 'Описание',
+ dataIndex: 'description',
+ key: 'description',
+ },
+ {
+ title: 'Разрешения',
+ dataIndex: 'permissions',
+ key: 'permissions',
+ render: (perms: Permission[]) => perms.map(p => p.name).join(', '),
+ },
+ {
+ title: 'Действия',
+ key: 'actions',
+ render: (_: any, record: Role) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
form.submit()}
+ onCancel={() => {
+ setIsModalVisible(false);
+ setEditingRole(null);
+ form.resetFields();
+ }}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+
+
+
+
+ {(permissions || []).map(permission => (
+
+
+ {permission.description}
+
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export default RoleManagement;
diff --git a/src/components/admin/UserManagement.tsx b/src/components/admin/UserManagement.tsx
new file mode 100644
index 0000000..452b734
--- /dev/null
+++ b/src/components/admin/UserManagement.tsx
@@ -0,0 +1,181 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, Select, Switch, App as AntdApp } from 'antd';
+import { useGet, usePost, usePatch } from '../../hooks/useApi';
+
+interface User {
+ id: number;
+ username: string;
+ role: string;
+ is_active: boolean;
+ categories: string[];
+}
+
+interface Role {
+ id: number;
+ name: string;
+}
+
+interface Category {
+ id: number;
+ name: string;
+}
+
+const UserManagement: React.FC = () => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для загрузки пользователей, ролей и категорий
+ const { data: users, execute: fetchUsers } = useGet('/admin/users');
+ const { data: roles, execute: fetchRoles } = useGet('/admin/roles');
+ const { data: categories, execute: fetchCategories } = useGet('/admin/categories');
+
+ // Используем `usePost` для создания пользователя
+ const { execute: createUser } = usePost('/admin/users');
+
+ // Используем `usePatch` для обновления статуса пользователя
+ const { execute: updateUserStatus } = usePatch(`/admin/users`);
+
+ // Загружаем данные при монтировании компонента
+ useEffect(() => {
+ fetchUsers();
+ fetchRoles();
+ fetchCategories();
+ }, []);
+
+ // Обработчик создания пользователя
+ const handleCreate = async (values: any) => {
+ setLoading(true);
+ try {
+ await createUser(values);
+ message.success('Пользователь создан');
+ setIsModalVisible(false);
+ form.resetFields();
+ fetchUsers();
+ } catch {
+ message.error('Ошибка при создании пользователя');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Обработчик изменения статуса пользователя (активен/не активен)
+ const handleToggleActive = async (userId: number, isActive: boolean) => {
+ try {
+ await updateUserStatus(`${userId}`, { is_active: isActive });
+ message.success('Статус пользователя обновлен');
+ fetchUsers();
+ } catch {
+ message.error('Ошибка при обновлении статуса');
+ }
+ };
+
+ // Определение столбцов таблицы
+ const columns = [
+ {
+ title: 'Имя пользователя',
+ dataIndex: 'username',
+ key: 'username',
+ },
+ {
+ title: 'Роль',
+ dataIndex: 'role',
+ key: 'role',
+ },
+ {
+ title: 'Активен',
+ dataIndex: 'is_active',
+ key: 'is_active',
+ render: (active: boolean, record: User) => (
+ handleToggleActive(record.id, checked)}
+ />
+ ),
+ },
+ {
+ title: 'Категории',
+ dataIndex: 'categories',
+ key: 'categories',
+ render: (categories: string[]) => categories.join(', '),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
form.submit()}
+ onCancel={() => setIsModalVisible(false)}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UserManagement;
diff --git a/src/components/admin/YearEndManagement.tsx b/src/components/admin/YearEndManagement.tsx
new file mode 100644
index 0000000..cc8c0c7
--- /dev/null
+++ b/src/components/admin/YearEndManagement.tsx
@@ -0,0 +1,88 @@
+import React, { useState, useEffect } from 'react';
+import { Button, Card, Descriptions, Alert, App as AntdApp } from 'antd';
+import { useGet, usePost } from '../../hooks/useApi';
+
+interface TransferStatus {
+ id: number;
+ year: number;
+ completed_at: string;
+ status: string;
+ error_message?: string;
+}
+
+const YearEndManagement: React.FC = () => {
+ const [loading, setLoading] = useState(false);
+ const [lastTransfer, setLastTransfer] = useState(null);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для получения статуса
+ const { data, error, execute: fetchStatus } = useGet('/year-end/status');
+
+ // Используем `usePost` для выполнения переноса
+ const { execute: transferData } = usePost('/year-end/transfer');
+
+ // Обновляем состояние, когда `data` загружено
+ useEffect(() => {
+ if (data) setLastTransfer(data);
+ }, [data]);
+
+ // Обработчик клика на кнопку переноса
+ const handleTransfer = async () => {
+ const currentYear = new Date().getFullYear();
+ const confirm = window.confirm(
+ `Вы уверены, что хотите выполнить перенос остатков за ${currentYear} год?`
+ );
+
+ if (!confirm) return;
+
+ setLoading(true);
+ try {
+ await transferData(); // Выполняем POST-запрос
+ message.success('Перенос остатков успешно выполнен');
+ fetchStatus(); // Обновляем статус
+ } catch {
+ message.error('Ошибка при переносе остатков');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {error && }
+
+ {lastTransfer && (
+
+ {lastTransfer.year}
+
+ {new Date(lastTransfer.completed_at).toLocaleString()}
+
+ {lastTransfer.status}
+ {lastTransfer.error_message && (
+
+ {lastTransfer.error_message}
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default YearEndManagement;
diff --git a/src/components/inventory/BoxManagement.tsx b/src/components/inventory/BoxManagement.tsx
new file mode 100644
index 0000000..065ffba
--- /dev/null
+++ b/src/components/inventory/BoxManagement.tsx
@@ -0,0 +1,230 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, InputNumber, Select, Space, App as AntdApp } from 'antd';
+import { useGet, usePost, usePut, useDelete } from '../../hooks/useApi';
+
+interface Item {
+ id: number;
+ name: string;
+ code: string;
+ quantity: number;
+}
+
+interface BoxItem {
+ id: number;
+ item_id: number;
+ quantity: number;
+ item: Item;
+}
+
+interface Box {
+ id: number;
+ name: string;
+ items: BoxItem[];
+}
+
+interface BoxManagementProps {
+ categoryId: number;
+ items: Item[];
+ onUpdate: () => void;
+}
+
+const BoxManagement: React.FC = ({ categoryId, items, onUpdate }) => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editingBox, setEditingBox] = useState(null);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для загрузки коробок
+ const { data: boxes, execute: fetchBoxes } = useGet(`/inventory/categories/${categoryId}/boxes`);
+
+ // Используем `usePost`, `usePut`, `useDelete`
+ const { execute: createBox } = usePost('/inventory/boxes');
+ const { execute: updateBox } = usePut(`/inventory/boxes/${editingBox?.id}`);
+ const { execute: deleteBox } = useDelete(`/inventory/boxes/${editingBox?.id}`);
+
+ // Загружаем данные при монтировании компонента
+ useEffect(() => {
+ fetchBoxes();
+ }, [categoryId]);
+
+ // Отправка формы (создание или обновление коробки)
+ const handleSubmit = async (values: any) => {
+ setLoading(true);
+ try {
+ const boxData = {
+ name: values.name,
+ category_id: categoryId,
+ items: values.items?.map((item: any) => ({
+ item_id: item.item_id,
+ quantity: item.quantity,
+ })) || [],
+ };
+
+ if (editingBox) {
+ await updateBox(boxData);
+ message.success('Коробка обновлена');
+ } else {
+ await createBox(boxData);
+ message.success('Коробка создана');
+ }
+ setIsModalVisible(false);
+ form.resetFields();
+ setEditingBox(null);
+ fetchBoxes();
+ onUpdate();
+ } catch {
+ message.error('Ошибка при сохранении коробки');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Открытие модального окна для редактирования коробки
+ const handleEdit = (box: Box) => {
+ setEditingBox(box);
+ form.setFieldsValue({
+ name: box.name,
+ items: box.items.map((item) => ({
+ item_id: item.item_id,
+ quantity: item.quantity,
+ })),
+ });
+ setIsModalVisible(true);
+ };
+
+ // Удаление коробки
+ const handleDelete = async (boxId: number) => {
+ try {
+ await deleteBox(boxId);
+ message.success('Коробка удалена');
+ fetchBoxes();
+ onUpdate();
+ } catch {
+ message.error('Ошибка при удалении коробки');
+ }
+ };
+
+ // Определение столбцов таблицы
+ const columns = [
+ {
+ title: 'Название коробки',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: 'Содержимое',
+ dataIndex: 'items',
+ key: 'items',
+ render: (items: BoxItem[]) => (
+
+ {items.map((item) => (
+ -
+ {item.item.name} - {item.quantity} шт.
+
+ ))}
+
+ ),
+ },
+ {
+ title: 'Действия',
+ key: 'actions',
+ render: (_: any, record: Box) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
form.submit()}
+ onCancel={() => {
+ setIsModalVisible(false);
+ setEditingBox(null);
+ form.resetFields();
+ }}
+ confirmLoading={loading}
+ width={800}
+ >
+
+
+
+
+
+ {(fields, { add, remove }) => (
+ <>
+ {fields.map(({ key, name, ...restField }) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default BoxManagement;
diff --git a/src/components/inventory/CategoryItems.tsx b/src/components/inventory/CategoryItems.tsx
new file mode 100644
index 0000000..21fe4d1
--- /dev/null
+++ b/src/components/inventory/CategoryItems.tsx
@@ -0,0 +1,206 @@
+import React, { useState, useEffect } from 'react';
+import { Table, Button, Modal, Form, Input, InputNumber, Space, Tabs, App as AntdApp } from 'antd';
+import { useGet, usePost, usePut, useDelete } from '../../hooks/useApi';
+import BoxManagement from './BoxManagement';
+
+interface Item {
+ id: number;
+ name: string;
+ code: string;
+ description: string;
+ quantity: number;
+ category_id: number;
+}
+
+interface CategoryItemsProps {
+ categoryId: number;
+}
+
+const CategoryItems: React.FC = ({ categoryId }) => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [editingItem, setEditingItem] = useState- (null);
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+ // Используем `useGet` для загрузки товаров
+ const { data: items, execute: fetchItems } = useGet
- (`/inventory/categories/${categoryId}/items`);
+
+ // Используем `usePost`, `usePut`, `useDelete` для управления товарами
+ const { execute: createItem } = usePost('/inventory/items');
+ const { execute: updateItem } = usePut(`/inventory/items/${editingItem?.id}`);
+ const { execute: deleteItem } = useDelete(`/inventory/items/${editingItem?.id}`);
+
+ // Загружаем данные при монтировании компонента
+ useEffect(() => {
+ fetchItems();
+ }, [categoryId]);
+
+ // Отправка формы (создание или обновление товара)
+ const handleSubmit = async (values: any) => {
+ setLoading(true);
+ try {
+ const itemData = {
+ ...values,
+ category_id: categoryId,
+ };
+
+ if (editingItem) {
+ await updateItem(itemData);
+ message.success('Товар обновлен');
+ } else {
+ await createItem(itemData);
+ message.success('Товар создан');
+ }
+
+ setIsModalVisible(false);
+ form.resetFields();
+ setEditingItem(null);
+ fetchItems();
+ } catch {
+ message.error('Ошибка при сохранении товара');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Открытие модального окна для редактирования товара
+ const handleEdit = (item: Item) => {
+ setEditingItem(item);
+ form.setFieldsValue({
+ name: item.name,
+ code: item.code,
+ description: item.description,
+ quantity: item.quantity,
+ });
+ setIsModalVisible(true);
+ };
+
+ // Удаление товара
+ const handleDelete = async (itemId: number) => {
+ try {
+ await deleteItem(itemId);
+ message.success('Товар удален');
+ fetchItems();
+ } catch {
+ message.error('Ошибка при удалении товара');
+ }
+ };
+
+ // Определение столбцов таблицы
+ const columns = [
+ {
+ title: 'Код',
+ dataIndex: 'code',
+ key: 'code',
+ },
+ {
+ title: 'Название',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: 'Описание',
+ dataIndex: 'description',
+ key: 'description',
+ },
+ {
+ title: 'Количество',
+ dataIndex: 'quantity',
+ key: 'quantity',
+ },
+ {
+ title: 'Действия',
+ key: 'actions',
+ render: (_: any, record: Item) => (
+
+
+
+
+ ),
+ },
+ ];
+
+ const tabItems = [
+ {
+ key: 'items',
+ label: 'Товары',
+ children: (
+ <>
+
+
+
+
+
+
+ form.submit()}
+ onCancel={() => {
+ setIsModalVisible(false);
+ setEditingItem(null);
+ form.resetFields();
+ }}
+ confirmLoading={loading}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
+ },
+ {
+ key: 'boxes',
+ label: 'Коробки',
+ children: ,
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
+
+export default CategoryItems;
diff --git a/src/components/reports/ReportGenerator.tsx b/src/components/reports/ReportGenerator.tsx
new file mode 100644
index 0000000..b197d8c
--- /dev/null
+++ b/src/components/reports/ReportGenerator.tsx
@@ -0,0 +1,165 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Form, Select, Button, Radio, App as AntdApp } from 'antd';
+import { DownloadOutlined } from '@ant-design/icons';
+import { useGet, useApi } from '../../hooks/useApi';
+
+interface Location {
+ id: number;
+ name: string;
+}
+
+interface Category {
+ id: number;
+ name: string;
+ location_id: number;
+}
+
+interface FormValues {
+ reportType: 'inventory' | 'boxes';
+ format: 'excel' | 'pdf';
+ locationId?: number;
+ categoryId?: number;
+}
+
+const ReportGenerator: React.FC = () => {
+ const [selectedLocation, setSelectedLocation] = useState();
+ const [loading, setLoading] = useState(false);
+ const { message } = AntdApp.useApp();
+
+ // Используем `useGet` для загрузки локаций
+ const { data: locations, execute: fetchLocations } = useGet('/locations');
+ const { data: categories, execute: fetchCategories } = useGet('');
+
+ // API-запрос для скачивания отчётов
+ const reportApi = useApi('', 'GET');
+
+ // Загружаем локации при монтировании компонента
+ useEffect(() => {
+ fetchLocations();
+ }, []);
+
+ useEffect(() => {
+ if (selectedLocation) {
+ fetchCategories(`/locations/${selectedLocation}/categories`);
+ } else {
+ fetchCategories('');
+ }
+ }, [selectedLocation]);
+
+ // Обработчик скачивания отчёта
+ const handleDownload = async (values: FormValues) => {
+ const { reportType, format, locationId, categoryId } = values;
+ setLoading(true);
+
+ try {
+ let url = '/reports/';
+ if (reportType === 'inventory') {
+ url += `inventory?format=${format}`;
+ if (locationId) url += `&locationId=${locationId}`;
+ if (categoryId) url += `&categoryId=${categoryId}`;
+ } else {
+ if (!categoryId) {
+ message.error('Выберите категорию для отчета по коробкам');
+ setLoading(false);
+ return;
+ }
+ url += `boxes/${categoryId}?format=${format}`;
+ }
+
+ // Выполняем запрос на скачивание отчета
+ const response = await reportApi.execute(url, {
+ headers: { 'Content-Type': 'application/json' },
+ responseType: 'blob', // Важно для скачивания файла
+ timeout: 30000, // Долгий таймаут для больших отчетов
+ });
+
+ if (!response) {
+ message.error('Ошибка при генерации отчета');
+ return;
+ }
+
+ // Создаем Blob и скачиваем файл
+ const blob = new Blob([response], {
+ type:
+ format === 'excel'
+ ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ : 'application/pdf',
+ });
+
+ const downloadUrl = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = downloadUrl;
+ link.download = `report-${new Date().toISOString()}.${format}`;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(downloadUrl);
+
+ message.success('Отчет успешно сгенерирован');
+ } catch {
+ message.error('Ошибка при генерации отчета');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Инвентарный отчет
+ Отчет по коробкам
+
+
+
+
+
+ Excel
+ PDF
+
+
+
+
+
+
+
+
+
+
+
+
+ } loading={loading} htmlType="submit">
+ Скачать отчет
+
+
+
+
+ );
+};
+
+export default ReportGenerator;
diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts
new file mode 100644
index 0000000..b4a8b8d
--- /dev/null
+++ b/src/hooks/useApi.ts
@@ -0,0 +1,97 @@
+import { useState, useCallback } from 'react';
+import axios from 'axios';
+
+const API_URL = import.meta.env.VITE_API_URL || '/api';
+
+interface ApiError {
+ message: string;
+}
+
+interface UseApiResponse {
+ data: T | null;
+ loading: boolean;
+ error: string | null;
+ execute: (...args: any[]) => Promise;
+}
+
+export function useApi(
+ endpoint: string,
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET'
+): UseApiResponse {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const execute = useCallback(
+ async (...args: any[]): Promise => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const token = localStorage.getItem('token');
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
+
+ let response;
+ switch (method) {
+ case 'GET':
+ response = await axios.get(`${API_URL}${endpoint}`, { headers });
+ break;
+ case 'POST':
+ response = await axios.post(`${API_URL}${endpoint}`, args[0], { headers });
+ break;
+ case 'PUT':
+ response = await axios.put(`${API_URL}${endpoint}`, args[0], { headers });
+ break;
+ case 'PATCH': // Добавляем поддержку PATCH-запросов
+ response = await axios.patch(`${API_URL}${endpoint}`, args[0], { headers });
+ break;
+ case 'DELETE':
+ response = await axios.delete(`${API_URL}${endpoint}`, { headers });
+ break;
+ default:
+ throw new Error(`Неподдерживаемый метод: ${method}`);
+ }
+
+ setData(response.data);
+ return response.data;
+ } catch (err: any) {
+ if (err && err.isAxiosError) {
+ const apiError = err.response?.data as ApiError | undefined;
+ const errorMessage = apiError?.message || 'Произошла ошибка на сервере';
+ setError(errorMessage);
+ } else {
+ setError('Произошла неизвестная ошибка');
+ }
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ },
+ [endpoint, method]
+ );
+
+ return { data, loading, error, execute };
+}
+
+// Вспомогательные функции для типичных запросов
+export function useGet(endpoint: string) {
+ return useApi(endpoint, 'GET');
+}
+
+export function usePost(endpoint: string) {
+ return useApi(endpoint, 'POST');
+}
+
+export function usePut(endpoint: string) {
+ return useApi(endpoint, 'PUT');
+}
+
+export function usePatch(endpoint: string) {
+ return useApi(endpoint, 'PATCH');
+}
+
+export function useDelete(endpoint: string) {
+ return useApi(endpoint, 'DELETE');
+}
+
+export default useApi;
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..dc3f4b5
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './styles/global.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/src/router.tsx b/src/router.tsx
new file mode 100644
index 0000000..4159bcc
--- /dev/null
+++ b/src/router.tsx
@@ -0,0 +1,50 @@
+import { createBrowserRouter } from 'react-router-dom';
+import Login from './components/Login';
+import Dashboard from './components/Dashboard';
+import AdminPanel from './components/admin/AdminPanel';
+import PrivateRoute from './components/PrivateRoute';
+
+const router = createBrowserRouter([
+ {
+ path: '/login',
+ element:
+ },
+ {
+ path: '/dashboard/*',
+ element: ,
+ children: [
+ {
+ path: ':categoryId',
+ element:
+ }
+ ]
+ },
+ {
+ path: '/admin',
+ element: ,
+ children: [
+ {
+ path: 'users',
+ element:
+ },
+ {
+ path: 'roles',
+ element:
+ },
+ {
+ path: 'categories',
+ element:
+ }
+ ]
+ },
+ {
+ path: '/',
+ element:
+ }
+], {
+ future: {
+ v7_relativeSplatPath: true
+ }
+});
+
+export default router;
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 0000000..b6a493b
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,12 @@
+body {
+ margin: 0;
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..f5e1a25
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5d1e601
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..862dfb2
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..66543b6
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,66 @@
+import { defineConfig, Plugin } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+import fs from 'fs';
+
+function fixSourceMaps(): Plugin {
+ let currentInterval: NodeJS.Timeout | null = null;
+
+ return {
+ name: 'fix-source-map',
+ enforce: 'post',
+ transform(source) {
+ if (currentInterval) {
+ return source;
+ }
+
+ currentInterval = setInterval(() => {
+ const nodeModulesPath = path.join(process.cwd(), 'node_modules', '.vite', 'deps');
+
+ if (fs.existsSync(nodeModulesPath)) {
+ clearInterval(currentInterval!);
+ currentInterval = null;
+
+ const files = fs.readdirSync(nodeModulesPath);
+ files.forEach((file) => {
+ const mapFile = `${file}.map`;
+ const mapPath = path.join(nodeModulesPath, mapFile);
+
+ if (fs.existsSync(mapPath)) {
+ let mapData = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
+
+ if (!mapData.sources || mapData.sources.length === 0) {
+ mapData.sources = [path.relative(mapPath, path.join(nodeModulesPath, file))];
+ fs.writeFileSync(mapPath, JSON.stringify(mapData), 'utf8');
+ }
+ }
+ });
+ }
+ }, 100);
+
+ return source;
+ }
+ };
+}
+
+export default defineConfig({
+ plugins: [
+ react(),
+ fixSourceMaps() as Plugin // Фикс source maps
+ ],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://127.0.0.1:4000',
+ changeOrigin: true,
+ secure: false,
+ ws: true
+ }
+ }
+ }
+ ,
+ resolve: {
+ preserveSymlinks: true // Иногда помогает с проблемами путей в Vite
+ }
+});