first commit
This commit is contained in:
parent
4bdbf8a8bc
commit
b2b36f325c
|
@ -0,0 +1,14 @@
|
|||
# Порт сервера
|
||||
PORT=4000
|
||||
|
||||
# База данных
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/warehouse"
|
||||
|
||||
# JWT секрет
|
||||
JWT_SECRET="your-secret-key"
|
||||
|
||||
# Код для регистрации админа
|
||||
ADMIN_CODE="admin123"
|
||||
|
||||
# Настройки CORS (опционально)
|
||||
CORS_ORIGIN="http://localhost:3000"
|
|
@ -0,0 +1,40 @@
|
|||
# Зависимости
|
||||
/node_modules
|
||||
/dist
|
||||
|
||||
# Env файлы
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Логи
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Prisma
|
||||
/prisma/*.db
|
||||
/prisma/migrations/*
|
||||
!/prisma/migrations/.gitkeep
|
||||
|
||||
# Временные файлы
|
||||
*.log
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Тестирование
|
||||
/coverage
|
||||
|
||||
# Production
|
||||
/build
|
|
@ -0,0 +1,15 @@
|
|||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# Генерируем Prisma Client
|
||||
RUN npx prisma generate
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
|
@ -0,0 +1,41 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "4000:4000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/warehouse
|
||||
- JWT_SECRET=your-secret-key
|
||||
- ADMIN_CODE=admin123
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- warehouse-network
|
||||
|
||||
db:
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=warehouse
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- warehouse-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
warehouse-network:
|
||||
name: warehouse-network
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"watch": ["src"],
|
||||
"ext": ".ts,.js",
|
||||
"ignore": [],
|
||||
"exec": "ts-node ./src/app.ts"
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "warehouse-management-backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.5.2",
|
||||
"@types/axios": "^0.9.36",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-schedule": "^2.1.1",
|
||||
"pdfkit": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/node": "^20.17.16",
|
||||
"@types/node-schedule": "^2.1.3",
|
||||
"@types/pdfkit": "^0.13.8",
|
||||
"nodemon": "^3.0.1",
|
||||
"prisma": "^5.5.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon",
|
||||
"build": "tsc",
|
||||
"start": "node dist/app.js",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Location {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
description String?
|
||||
categories Category[]
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@map("locations")
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
location Location @relation(fields: [location_id], references: [id])
|
||||
location_id Int
|
||||
items Item[]
|
||||
boxes Box[]
|
||||
userCategoryAccess UserCategoryAccess[]
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("categories")
|
||||
}
|
||||
|
||||
model Item {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
code String?
|
||||
description String?
|
||||
quantity Int @default(0)
|
||||
category Category @relation(fields: [category_id], references: [id])
|
||||
category_id Int
|
||||
box_items BoxItem[]
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
@@map("items")
|
||||
}
|
||||
|
||||
model Box {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
category Category @relation(fields: [category_id], references: [id])
|
||||
category_id Int
|
||||
items BoxItem[]
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("boxes")
|
||||
}
|
||||
|
||||
model BoxItem {
|
||||
id Int @id @default(autoincrement())
|
||||
box Box @relation(fields: [box_id], references: [id])
|
||||
box_id Int
|
||||
item Item @relation(fields: [item_id], references: [id])
|
||||
item_id Int
|
||||
quantity Int
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("box_items")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
description String?
|
||||
users User[]
|
||||
permissions RolePermission[]
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
description String?
|
||||
roles RolePermission[]
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
id Int @id @default(autoincrement())
|
||||
role Role @relation(fields: [role_id], references: [id])
|
||||
role_id Int
|
||||
permission Permission @relation(fields: [permission_id], references: [id])
|
||||
permission_id Int
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@unique([role_id, permission_id])
|
||||
@@map("role_permissions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password_hash String
|
||||
role Role @relation(fields: [role_id], references: [id])
|
||||
role_id Int
|
||||
is_active Boolean @default(true)
|
||||
last_login DateTime?
|
||||
categoryAccess UserCategoryAccess[]
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model UserCategoryAccess {
|
||||
id Int @id @default(autoincrement())
|
||||
user User @relation(fields: [user_id], references: [id])
|
||||
user_id Int
|
||||
category Category @relation(fields: [category_id], references: [id])
|
||||
category_id Int
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("user_category_access")
|
||||
}
|
||||
|
||||
model YearEndTransfer {
|
||||
id Int @id @default(autoincrement())
|
||||
year Int
|
||||
completed_at DateTime
|
||||
status String
|
||||
error_message String?
|
||||
created_at DateTime @default(now())
|
||||
|
||||
@@map("year_end_transfers")
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import YearEndService from './services/YearEndService';
|
||||
|
||||
// Импорт маршрутов
|
||||
import authRoutes from './routes/auth.routes';
|
||||
import adminRoutes from './routes/admin.routes';
|
||||
import inventoryRoutes from './routes/inventory.routes';
|
||||
import reportRoutes from './routes/report.routes';
|
||||
import yearEndRoutes from './routes/yearEnd.routes';
|
||||
|
||||
const app = express();
|
||||
const prisma = new PrismaClient();
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors({
|
||||
origin: 'http://localhost:3000', // Разрешаем запросы с фронта
|
||||
credentials: true, // Разрешаем куки и авторизацию
|
||||
methods: 'GET,POST,PUT,DELETE,OPTIONS', // Разрешенные методы
|
||||
allowedHeaders: 'Content-Type,Authorization' // Разрешенные заголовки
|
||||
}));
|
||||
app.options('*', (req, res) => {
|
||||
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
res.header('Access-Control-Allow-Credentials', 'true');
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// API маршруты
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
app.use('/api/inventory', inventoryRoutes);
|
||||
app.use('/api/reports', reportRoutes);
|
||||
app.use('/api/year-end', yearEndRoutes);
|
||||
|
||||
// Раздача статических файлов в production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '../../frontend/dist')));
|
||||
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация планировщика переноса остатков
|
||||
YearEndService.initScheduler();
|
||||
|
||||
// Обработка ошибок
|
||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ message: 'Что-то пошло не так!' });
|
||||
});
|
||||
|
||||
// Запуск сервера
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM received. Closing HTTP server and database connections...');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
|
@ -0,0 +1,378 @@
|
|||
import { Request, Response } from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
export class AdminController {
|
||||
static async getUsers(req: Request, res: Response) {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
include: {
|
||||
role: true,
|
||||
categoryAccess: {
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const formattedUsers = users.map(user => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role.name,
|
||||
isActive: user.is_active,
|
||||
lastLogin: user.last_login,
|
||||
categories: user.categoryAccess.map(access => access.category.name)
|
||||
}));
|
||||
|
||||
res.json(formattedUsers);
|
||||
} catch (error) {
|
||||
console.error('Error getting users:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async createUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, password, roleId, categoryIds } = req.body;
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: 'Пользователь уже существует' });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password_hash: passwordHash,
|
||||
role_id: roleId,
|
||||
is_active: true,
|
||||
categoryAccess: {
|
||||
create: categoryIds.map((categoryId: number) => ({
|
||||
category_id: categoryId
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
categoryAccess: {
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const formattedUser = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role.name,
|
||||
isActive: user.is_active,
|
||||
lastLogin: user.last_login,
|
||||
categories: user.categoryAccess.map(access => access.category.name)
|
||||
};
|
||||
|
||||
res.json(formattedUser);
|
||||
} catch (error) {
|
||||
console.error('Error creating user:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async updateUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { password, roleId, isActive, categoryIds } = req.body;
|
||||
|
||||
const updateData: any = {
|
||||
role_id: roleId,
|
||||
is_active: isActive
|
||||
};
|
||||
|
||||
if (password) {
|
||||
updateData.password_hash = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
...updateData,
|
||||
categoryAccess: {
|
||||
deleteMany: {}, // Удаляем текущие доступы
|
||||
create: categoryIds.map((categoryId: number) => ({
|
||||
category_id: categoryId
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
categoryAccess: {
|
||||
include: {
|
||||
category: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const formattedUser = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role.name,
|
||||
isActive: user.is_active,
|
||||
lastLogin: user.last_login,
|
||||
categories: user.categoryAccess.map(access => access.category.name)
|
||||
};
|
||||
|
||||
res.json({ message: 'Пользователь успешно обновлен', user: formattedUser });
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteUser(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
await prisma.user.delete({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
res.json({ message: 'Пользователь успешно удален' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async getRoles(req: Request, res: Response) {
|
||||
try {
|
||||
const roles = await prisma.role.findMany({
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
res.json(roles);
|
||||
} catch (error) {
|
||||
console.error('Error fetching roles:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async getPermissions(req: Request, res: Response) {
|
||||
try {
|
||||
const permissions = await prisma.permission.findMany();
|
||||
res.json(permissions);
|
||||
} catch (error) {
|
||||
console.error('Error fetching permissions:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async createRole(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const existingRole = await prisma.role.findUnique({
|
||||
where: { name }
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
return res.status(400).json({ message: 'Роль с таким названием уже существует' });
|
||||
}
|
||||
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
permissions: {
|
||||
create: permissions.map((permissionId: number) => ({
|
||||
permission_id: permissionId
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json(role);
|
||||
} catch (error) {
|
||||
console.error('Error creating role:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async updateRole(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const role = await prisma.role.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (role?.name === 'admin') {
|
||||
return res.status(403).json({ message: 'Роль администратора нельзя изменить' });
|
||||
}
|
||||
|
||||
await prisma.rolePermission.deleteMany({
|
||||
where: { role_id: Number(id) }
|
||||
});
|
||||
|
||||
const updatedRole = await prisma.role.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
permissions: {
|
||||
create: permissions.map((permissionId: number) => ({
|
||||
permission_id: permissionId
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json(updatedRole);
|
||||
} catch (error) {
|
||||
console.error('Error updating role:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteRole(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const role = await prisma.role.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (role?.name === 'admin') {
|
||||
return res.status(403).json({ message: 'Роль администратора нельзя удалить' });
|
||||
}
|
||||
|
||||
await prisma.role.delete({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
res.json({ message: 'Роль успешно удалена' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting role:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async getLocations(req: Request, res: Response) {
|
||||
try {
|
||||
const locations = await prisma.location.findMany();
|
||||
res.json(locations);
|
||||
} catch (error) {
|
||||
console.error('Error fetching locations:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async getCategories(req: Request, res: Response) {
|
||||
try {
|
||||
const categories = await prisma.category.findMany({
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
});
|
||||
res.json(categories);
|
||||
} catch (error) {
|
||||
console.error('Error fetching categories:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async createCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, location_id } = req.body;
|
||||
|
||||
const category = await prisma.category.create({
|
||||
data: {
|
||||
name,
|
||||
location_id
|
||||
},
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(category);
|
||||
} catch (error) {
|
||||
console.error('Error creating category:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async updateCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, location_id } = req.body;
|
||||
|
||||
const category = await prisma.category.update({
|
||||
where: { id: Number(id) },
|
||||
data: {
|
||||
name,
|
||||
location_id
|
||||
},
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json(category);
|
||||
} catch (error) {
|
||||
console.error('Error updating category:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteCategory(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const itemsCount = await prisma.item.count({
|
||||
where: { category_id: Number(id) }
|
||||
});
|
||||
|
||||
if (itemsCount > 0) {
|
||||
return res.status(400).json({
|
||||
message: 'Невозможно удалить категорию, содержащую товары'
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.category.delete({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
res.json({ message: 'Категория успешно удалена' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting category:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
class AuthController {
|
||||
/**
|
||||
* Регистрация нового пользователя (доступ только по adminCode)
|
||||
*/
|
||||
static async register(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, password, adminCode } = req.body;
|
||||
|
||||
// Проверка наличия данных
|
||||
if (!username || !password || !adminCode) {
|
||||
return res.status(400).json({ message: 'Необходимо указать имя пользователя, пароль и код доступа' });
|
||||
}
|
||||
|
||||
// Проверяем adminCode
|
||||
if (adminCode !== process.env.ADMIN_CODE) {
|
||||
return res.status(403).json({ message: 'Неверный код доступа' });
|
||||
}
|
||||
|
||||
// Проверяем, существует ли пользователь
|
||||
const existingUser = await prisma.user.findUnique({ where: { username } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: 'Пользователь с таким именем уже существует' });
|
||||
}
|
||||
|
||||
// Ищем роль "user" (по умолчанию)
|
||||
const userRole = await prisma.role.findFirst({ where: { name: 'user' } });
|
||||
if (!userRole) return res.status(500).json({ message: 'Роль пользователя не найдена' });
|
||||
|
||||
// Хешируем пароль
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Создаем пользователя
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username,
|
||||
password_hash: hashedPassword,
|
||||
role_id: userRole.id,
|
||||
is_active: true,
|
||||
},
|
||||
include: {
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Генерируем JWT токен
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username, role: user.role.name },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Пользователь успешно зарегистрирован',
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role.name,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при регистрации:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Авторизация пользователя (логин)
|
||||
*/
|
||||
static async login(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ message: 'Необходимо указать имя пользователя и пароль' });
|
||||
}
|
||||
|
||||
// Поиск пользователя
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username },
|
||||
include: { role: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: 'Неверное имя пользователя или пароль' });
|
||||
}
|
||||
|
||||
// Проверка активности
|
||||
if (!user.is_active) {
|
||||
return res.status(403).json({ message: 'Учетная запись деактивирована' });
|
||||
}
|
||||
|
||||
// Проверка пароля
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ message: 'Неверное имя пользователя или пароль' });
|
||||
}
|
||||
|
||||
// Обновляем last_login
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { last_login: new Date() },
|
||||
});
|
||||
|
||||
// Генерация токена
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username, role: user.role.name },
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role.name,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при входе:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация JWT токена
|
||||
*/
|
||||
static async validateToken(req: Request, res: Response) {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Токен не предоставлен' });
|
||||
}
|
||||
const secretKey: string = process.env.JWT_SECRET as string;
|
||||
const decoded = jwt.verify(token, secretKey);
|
||||
res.json({ valid: true, user: decoded });
|
||||
} catch (error) {
|
||||
res.status(401).json({ valid: false, message: 'Недействительный токен' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthController;
|
|
@ -0,0 +1,299 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../utils/prisma';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
// Типы для расширенных моделей с включенными связями
|
||||
type ItemWithRelations = Prisma.ItemGetPayload<{
|
||||
include: {
|
||||
category: true;
|
||||
box_items: {
|
||||
include: {
|
||||
box: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
type BoxWithRelations = Prisma.BoxGetPayload<{
|
||||
include: {
|
||||
category: true;
|
||||
items: {
|
||||
include: {
|
||||
item: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
// Типы для входящих данных
|
||||
interface CreateItemData {
|
||||
name: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
categoryId: number;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
interface UpdateItemData {
|
||||
name?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
quantity?: number;
|
||||
}
|
||||
|
||||
interface BoxItemInput {
|
||||
id: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface CreateBoxData {
|
||||
name: string;
|
||||
categoryId: number;
|
||||
items: BoxItemInput[];
|
||||
}
|
||||
|
||||
interface UpdateBoxData {
|
||||
name?: string;
|
||||
items: BoxItemInput[];
|
||||
}
|
||||
|
||||
class InventoryController {
|
||||
// Получение товаров категории
|
||||
static async getCategoryItems(req: Request, res: Response) {
|
||||
try {
|
||||
const categoryId = parseInt(req.params.categoryId, 10);
|
||||
if (isNaN(categoryId)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор категории' });
|
||||
}
|
||||
|
||||
const items: ItemWithRelations[] = await prisma.item.findMany({
|
||||
where: { category_id: categoryId },
|
||||
include: {
|
||||
category: true,
|
||||
box_items: { include: { box: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json(
|
||||
items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: item.description,
|
||||
quantity: item.quantity,
|
||||
category: item.category?.name || 'Без категории',
|
||||
boxes: item.box_items.map((bi) => ({
|
||||
id: bi.box.id,
|
||||
name: bi.box.name,
|
||||
quantity: bi.quantity,
|
||||
})),
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении товаров категории:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Создание нового товара
|
||||
static async createItem(req: Request<{}, {}, CreateItemData>, res: Response) {
|
||||
try {
|
||||
const { name, code, description, categoryId, quantity = 0 } = req.body;
|
||||
|
||||
if (!name || !categoryId) {
|
||||
return res.status(400).json({ message: 'Необходимо указать название и категорию' });
|
||||
}
|
||||
|
||||
const category_id = parseInt(String(categoryId), 10);
|
||||
if (isNaN(category_id)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор категории' });
|
||||
}
|
||||
|
||||
const item = await prisma.item.create({
|
||||
data: { name, code, description, quantity, category_id },
|
||||
include: { category: true },
|
||||
});
|
||||
|
||||
res.json(item);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании товара:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление товара
|
||||
static async updateItem(req: Request<{ id: string }, {}, UpdateItemData>, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор товара' });
|
||||
}
|
||||
|
||||
const item: ItemWithRelations = await prisma.item.update({
|
||||
where: { id },
|
||||
data: req.body,
|
||||
include: {
|
||||
category: true,
|
||||
box_items: { include: { box: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json(item);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2025') {
|
||||
return res.status(404).json({ message: 'Товар не найден' });
|
||||
}
|
||||
}
|
||||
console.error('Ошибка при обновлении товара:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление товара
|
||||
static async deleteItem(req: Request, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор товара' });
|
||||
}
|
||||
|
||||
const boxItems = await prisma.boxItem.findMany({ where: { item_id: id } });
|
||||
|
||||
if (boxItems.length > 0) {
|
||||
return res.status(400).json({ message: 'Невозможно удалить товар, который находится в коробках' });
|
||||
}
|
||||
|
||||
await prisma.item.delete({ where: { id } });
|
||||
|
||||
res.json({ message: 'Товар успешно удален' });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении товара:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Получение коробок категории
|
||||
static async getCategoryBoxes(req: Request, res: Response) {
|
||||
try {
|
||||
const categoryId = parseInt(req.params.categoryId, 10);
|
||||
if (isNaN(categoryId)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор категории' });
|
||||
}
|
||||
|
||||
const boxes: BoxWithRelations[] = await prisma.box.findMany({
|
||||
where: { category_id: categoryId },
|
||||
include: {
|
||||
category: true,
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json(
|
||||
boxes.map(box => ({
|
||||
id: box.id,
|
||||
name: box.name,
|
||||
category: box.category?.name,
|
||||
items: box.items.map(bi => ({
|
||||
id: bi.item.id,
|
||||
name: bi.item.name,
|
||||
quantity: bi.quantity,
|
||||
})),
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении коробок категории:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Создание новой коробки
|
||||
static async createBox(req: Request<{}, {}, CreateBoxData>, res: Response) {
|
||||
try {
|
||||
const { name, categoryId, items } = req.body;
|
||||
|
||||
if (!name || !categoryId || !Array.isArray(items)) {
|
||||
return res.status(400).json({ message: 'Некорректные входные данные' });
|
||||
}
|
||||
|
||||
const box: BoxWithRelations = await prisma.box.create({
|
||||
data: {
|
||||
name,
|
||||
category_id: categoryId,
|
||||
items: {
|
||||
create: items.map((item: BoxItemInput) => ({
|
||||
item_id: item.id,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
category: true,
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json(box);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании коробки:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление коробки
|
||||
static async updateBox(req: Request<{ id: string }, {}, UpdateBoxData>, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор коробки' });
|
||||
}
|
||||
|
||||
const { name, items } = req.body;
|
||||
|
||||
const box = await prisma.$transaction(async (prisma) => {
|
||||
await prisma.boxItem.deleteMany({ where: { box_id: id } });
|
||||
|
||||
return prisma.box.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
items: {
|
||||
create: items.map((item: BoxItemInput) => ({
|
||||
item_id: item.id,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
category: true,
|
||||
items: { include: { item: true } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
res.json(box);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении коробки:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление коробки
|
||||
static async deleteBox(req: Request, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор коробки' });
|
||||
}
|
||||
|
||||
await prisma.box.delete({ where: { id } });
|
||||
|
||||
res.json({ message: 'Коробка успешно удалена' });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении коробки:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default InventoryController;
|
|
@ -0,0 +1,123 @@
|
|||
import { Request, Response } from 'express';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
class LocationController {
|
||||
static async getAll(req: Request, res: Response) {
|
||||
try {
|
||||
const locations = await prisma.location.findMany({
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
res.json(locations);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении локаций:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async create(req: Request, res: Response) {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ message: 'Название локации обязательно' });
|
||||
}
|
||||
|
||||
// Проверяем уникальность имени
|
||||
const existingLocation = await prisma.location.findFirst({
|
||||
where: { name }
|
||||
});
|
||||
|
||||
if (existingLocation) {
|
||||
return res.status(400).json({ message: 'Локация с таким названием уже существует' });
|
||||
}
|
||||
|
||||
const location = await prisma.location.create({
|
||||
data: {
|
||||
name,
|
||||
description
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json(location);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании локации:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async update(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, description } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ message: 'Название локации обязательно' });
|
||||
}
|
||||
|
||||
// Проверяем существование локации
|
||||
const existingLocation = await prisma.location.findUnique({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
if (!existingLocation) {
|
||||
return res.status(404).json({ message: 'Локация не найдена' });
|
||||
}
|
||||
|
||||
// Проверяем уникальность нового имени
|
||||
const duplicateName = await prisma.location.findFirst({
|
||||
where: {
|
||||
name,
|
||||
NOT: { id: Number(id) }
|
||||
}
|
||||
});
|
||||
|
||||
if (duplicateName) {
|
||||
return res.status(400).json({ message: 'Локация с таким названием уже существует' });
|
||||
}
|
||||
|
||||
const location = await prisma.location.update({
|
||||
where: { id: Number(id) },
|
||||
data: { name, description }
|
||||
});
|
||||
|
||||
res.json(location);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении локации:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async delete(req: Request, res: Response) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Проверяем существование локации
|
||||
const location = await prisma.location.findUnique({
|
||||
where: { id: Number(id) },
|
||||
include: { categories: true }
|
||||
});
|
||||
|
||||
if (!location) {
|
||||
return res.status(404).json({ message: 'Локация не найдена' });
|
||||
}
|
||||
|
||||
// Проверяем, есть ли связанные категории
|
||||
if (location.categories.length > 0) {
|
||||
return res.status(400).json({
|
||||
message: 'Невозможно удалить локацию, содержащую категории'
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.location.delete({
|
||||
where: { id: Number(id) }
|
||||
});
|
||||
|
||||
res.json({ message: 'Локация успешно удалена' });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении локации:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LocationController;
|
|
@ -0,0 +1,124 @@
|
|||
import { Request, Response } from 'express';
|
||||
import ReportService from '../services/ReportService';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
class ReportController {
|
||||
static async generateInventoryReport(req: Request, res: Response) {
|
||||
try {
|
||||
const { categoryId, format = 'excel' } = req.query;
|
||||
const parsedCategoryId = categoryId ? parseInt(categoryId as string, 10) : undefined;
|
||||
|
||||
if (categoryId && isNaN(parsedCategoryId!)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор категории' });
|
||||
}
|
||||
|
||||
let buffer: Buffer;
|
||||
if (format === 'excel') {
|
||||
buffer = await ReportService.generateInventoryReport(parsedCategoryId);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=inventory-report.xlsx');
|
||||
} else if (format === 'pdf') {
|
||||
const items = await prisma.item.findMany({
|
||||
// Используем parsedCategoryId вместо categoryId
|
||||
where: parsedCategoryId !== undefined ? { category_id: parsedCategoryId } : undefined,
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
},
|
||||
box_items: {
|
||||
include: {
|
||||
box: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
buffer = await ReportService.generatePDFReport(items, 'inventory');
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=inventory-report.pdf');
|
||||
} else {
|
||||
return res.status(400).json({ message: 'Неподдерживаемый формат отчета' });
|
||||
}
|
||||
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации отчета по инвентарю:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async generateBoxReport(req: Request, res: Response) {
|
||||
try {
|
||||
const { categoryId, format = 'excel' } = req.query;
|
||||
|
||||
if (!categoryId) {
|
||||
return res.status(400).json({ message: 'Необходимо указать категорию' });
|
||||
}
|
||||
|
||||
const parsedCategoryId = parseInt(categoryId as string, 10);
|
||||
if (isNaN(parsedCategoryId)) {
|
||||
return res.status(400).json({ message: 'Некорректный идентификатор категории' });
|
||||
}
|
||||
|
||||
let buffer: Buffer;
|
||||
if (format === 'excel') {
|
||||
buffer = await ReportService.generateBoxReport(parsedCategoryId);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=box-report.xlsx');
|
||||
} else if (format === 'pdf') {
|
||||
const boxes = await prisma.box.findMany({
|
||||
where: { category_id: parsedCategoryId },
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true
|
||||
}
|
||||
},
|
||||
items: {
|
||||
include: {
|
||||
item: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
buffer = await ReportService.generatePDFReport(boxes, 'boxes');
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=box-report.pdf');
|
||||
} else {
|
||||
return res.status(400).json({ message: 'Неподдерживаемый формат отчета' });
|
||||
}
|
||||
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации отчета по коробкам:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
|
||||
static async generateYearEndReport(req: Request, res: Response) {
|
||||
try {
|
||||
const { year, format = 'excel' } = req.query;
|
||||
|
||||
if (!year) {
|
||||
return res.status(400).json({ message: 'Необходимо указать год' });
|
||||
}
|
||||
|
||||
const parsedYear = parseInt(year as string, 10);
|
||||
if (isNaN(parsedYear)) {
|
||||
return res.status(400).json({ message: 'Некорректный формат года' });
|
||||
}
|
||||
|
||||
const buffer = await ReportService.generateYearEndReport(parsedYear);
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=year-end-report-${parsedYear}.xlsx`);
|
||||
res.send(buffer);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации годового отчета:', error);
|
||||
res.status(500).json({ message: 'Ошибка сервера' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportController;
|
|
@ -0,0 +1,26 @@
|
|||
import { Request, Response } from 'express';
|
||||
import YearEndService from '../services/YearEndService';
|
||||
|
||||
class YearEndController {
|
||||
static async initiateTransfer(req: Request, res: Response) {
|
||||
try {
|
||||
await YearEndService.transferRemainders();
|
||||
res.json({ message: 'Перенос остатков успешно выполнен' });
|
||||
} catch (error) {
|
||||
console.error('Error initiating transfer:', error);
|
||||
res.status(500).json({ message: 'Ошибка при переносе остатков' });
|
||||
}
|
||||
}
|
||||
|
||||
static async getTransferStatus(req: Request, res: Response) {
|
||||
try {
|
||||
const status = await YearEndService.getLastTransferStatus();
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
console.error('Error getting transfer status:', error);
|
||||
res.status(500).json({ message: 'Ошибка при получении статуса переноса' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default YearEndController;
|
|
@ -0,0 +1,31 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Токен не предоставлен' });
|
||||
}
|
||||
const secretKey: string = process.env.JWT_SECRET as string;
|
||||
const decoded = jwt.verify(token, secretKey);
|
||||
(req as any).user = decoded;
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: 'Недействительный токен' });
|
||||
}
|
||||
};
|
||||
|
||||
export const roleMiddleware = (roles: string[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userRole = (req as any).user.role;
|
||||
if (!roles.includes(userRole)) {
|
||||
return res.status(403).json({ message: 'Доступ запрещен' });
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(403).json({ message: 'Доступ запрещен' });
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import { Router } from 'express';
|
||||
import { AdminController } from '../controllers/admin.controller';
|
||||
import { authMiddleware, roleMiddleware } from '../middleware/auth.middleware';
|
||||
import LocationController from '../controllers/location.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(roleMiddleware(['admin']));
|
||||
|
||||
// Маршруты для управления пользователями
|
||||
router.get('/users', AdminController.getUsers);
|
||||
router.post('/users', AdminController.createUser);
|
||||
router.put('/users/:id', AdminController.updateUser);
|
||||
router.delete('/users/:id', AdminController.deleteUser);
|
||||
|
||||
// Маршруты для управления ролями
|
||||
router.get('/roles', AdminController.getRoles);
|
||||
router.post('/roles', AdminController.createRole);
|
||||
router.put('/roles/:id', AdminController.updateRole);
|
||||
router.delete('/roles/:id', AdminController.deleteRole);
|
||||
|
||||
// Маршруты для получения разрешений
|
||||
router.get('/permissions', AdminController.getPermissions);
|
||||
|
||||
// Маршруты для управления локациями
|
||||
router.get('/locations', LocationController.getAll);
|
||||
router.post('/locations', LocationController.create);
|
||||
router.put('/locations/:id', LocationController.update);
|
||||
router.delete('/locations/:id', LocationController.delete);
|
||||
|
||||
// Маршруты для управления категориями
|
||||
router.get('/categories', AdminController.getCategories);
|
||||
router.post('/categories', AdminController.createCategory);
|
||||
router.put('/categories/:id', AdminController.updateCategory);
|
||||
router.delete('/categories/:id', AdminController.deleteCategory);
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,10 @@
|
|||
import { Router } from 'express';
|
||||
import AuthController from '../controllers/auth.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', AuthController.login);
|
||||
router.post('/register', AuthController.register);
|
||||
router.post('/validate', AuthController.validateToken);
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,21 @@
|
|||
import { Router } from 'express';
|
||||
import InventoryController from '../controllers/inventory.controller';
|
||||
import { authMiddleware } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Маршруты для товаров
|
||||
router.get('/categories/:categoryId/items', InventoryController.getCategoryItems);
|
||||
router.post('/items', InventoryController.createItem);
|
||||
router.put('/items/:id', InventoryController.updateItem);
|
||||
router.delete('/items/:id', InventoryController.deleteItem);
|
||||
|
||||
// Маршруты для коробок
|
||||
router.get('/categories/:categoryId/boxes', InventoryController.getCategoryBoxes);
|
||||
router.post('/boxes', InventoryController.createBox);
|
||||
router.put('/boxes/:id', InventoryController.updateBox);
|
||||
router.delete('/boxes/:id', InventoryController.deleteBox);
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,10 @@
|
|||
import { Router } from 'express';
|
||||
import { authMiddleware } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Здесь будут маршруты отчетов
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,15 @@
|
|||
import { Router } from 'express';
|
||||
import YearEndController from '../controllers/yearEnd.controller';
|
||||
import { authMiddleware, roleMiddleware } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(roleMiddleware(['admin'])); // Только админ может управлять переносом остатков
|
||||
|
||||
router.post('/transfer', YearEndController.initiateTransfer);
|
||||
router.get('/status', YearEndController.getTransferStatus);
|
||||
|
||||
// Здесь будут маршруты переноса остатков
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,369 @@
|
|||
import { prisma } from '../utils/prisma';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import ExcelJS from 'exceljs';
|
||||
import PDFDocument from 'pdfkit';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type ItemWithRelations = Prisma.ItemGetPayload<{
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true;
|
||||
};
|
||||
};
|
||||
box_items: {
|
||||
include: {
|
||||
box: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
type BoxWithRelations = Prisma.BoxGetPayload<{
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true;
|
||||
};
|
||||
};
|
||||
items: {
|
||||
include: {
|
||||
item: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
class ReportService {
|
||||
static async generateInventoryReport(categoryId?: number): Promise<Buffer> {
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Инвентарь');
|
||||
|
||||
// Настройка заголовков
|
||||
worksheet.columns = [
|
||||
{ header: 'ID', key: 'id', width: 10 },
|
||||
{ header: 'Название', key: 'name', width: 30 },
|
||||
{ header: 'Код', key: 'code', width: 15 },
|
||||
{ header: 'Описание', key: 'description', width: 40 },
|
||||
{ header: 'Количество', key: 'quantity', width: 15 },
|
||||
{ header: 'Категория', key: 'category', width: 20 },
|
||||
{ header: 'Коробки', key: 'boxes', width: 40 },
|
||||
];
|
||||
|
||||
// Получение данных
|
||||
const items: ItemWithRelations[] = await prisma.item.findMany({
|
||||
where: categoryId ? { category_id: categoryId } : undefined,
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true,
|
||||
},
|
||||
},
|
||||
box_items: {
|
||||
include: {
|
||||
box: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Заполнение данными
|
||||
items.forEach((item) => {
|
||||
worksheet.addRow({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
quantity: item.quantity,
|
||||
category: item.category?.name || 'Без категории',
|
||||
boxes: item.box_items
|
||||
.map((bi) => `${bi.box.name} (${bi.quantity})`)
|
||||
.join(', '),
|
||||
});
|
||||
});
|
||||
|
||||
// Стилизация
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true };
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
|
||||
// Возвращаем буфер
|
||||
return await workbook.xlsx.writeBuffer() as Buffer;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации отчета по инвентарю:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async generateBoxReport(categoryId: number): Promise<Buffer> { // Исправленный тип
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Коробки');
|
||||
|
||||
// Настройка заголовков
|
||||
worksheet.columns = [
|
||||
{ header: 'ID коробки', key: 'boxId', width: 10 },
|
||||
{ header: 'Название коробки', key: 'boxName', width: 30 },
|
||||
{ header: 'Категория', key: 'category', width: 20 },
|
||||
{ header: 'Товар', key: 'itemName', width: 30 },
|
||||
{ header: 'Код товара', key: 'itemCode', width: 15 },
|
||||
{ header: 'Количество', key: 'quantity', width: 15 },
|
||||
];
|
||||
|
||||
// Получение данных
|
||||
const boxes: BoxWithRelations[] = await prisma.box.findMany({
|
||||
where: { category_id: categoryId },
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Заполнение данными
|
||||
boxes.forEach((box) => {
|
||||
box.items.forEach((boxItem) => {
|
||||
worksheet.addRow({
|
||||
boxId: box.id,
|
||||
boxName: box.name,
|
||||
category: box.category?.name || 'Без категории',
|
||||
itemName: boxItem.item.name,
|
||||
itemCode: boxItem.item.code || '',
|
||||
quantity: boxItem.quantity,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Стилизация
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true };
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
|
||||
// Возвращаем буфер
|
||||
return await workbook.xlsx.writeBuffer() as Buffer;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации отчета по коробкам:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async generateYearEndReport(year: number): Promise<Buffer> {
|
||||
try {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet(`Остатки ${year}`);
|
||||
|
||||
// Настройка заголовков
|
||||
worksheet.columns = [
|
||||
{ header: 'Категория', key: 'category', width: 30 },
|
||||
{ header: 'Товар', key: 'name', width: 30 },
|
||||
{ header: 'Код', key: 'code', width: 15 },
|
||||
{ header: 'Описание', key: 'description', width: 40 },
|
||||
{ header: 'Количество', key: 'quantity', width: 15 },
|
||||
];
|
||||
|
||||
// Получение данных
|
||||
const items: ItemWithRelations[] = await prisma.item.findMany({
|
||||
where: {
|
||||
category: {
|
||||
name: {
|
||||
startsWith: `${year} Остатки`,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
category: {
|
||||
include: {
|
||||
location: true,
|
||||
},
|
||||
},
|
||||
box_items: {
|
||||
include: {
|
||||
box: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Заполнение данными
|
||||
items.forEach((item) => {
|
||||
worksheet.addRow({
|
||||
category: item.category?.name || 'Без категории',
|
||||
name: item.name,
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
quantity: item.quantity,
|
||||
});
|
||||
});
|
||||
|
||||
// Стилизация
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true };
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
|
||||
// Возвращаем буфер
|
||||
return await workbook.xlsx.writeBuffer() as Buffer;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при генерации годового отчета:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async generateExcelReport(
|
||||
data: ItemWithRelations[] | BoxWithRelations[],
|
||||
type: 'inventory' | 'boxes'
|
||||
): Promise<Buffer> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Report');
|
||||
|
||||
if (type === 'inventory') {
|
||||
// Настройка колонок для инвентарного отчета
|
||||
worksheet.columns = [
|
||||
{ header: 'Локация', key: 'location', width: 15 },
|
||||
{ header: 'Категория', key: 'category', width: 20 },
|
||||
{ header: 'Код', key: 'code', width: 15 },
|
||||
{ header: 'Название', key: 'name', width: 30 },
|
||||
{ header: 'Количество', key: 'quantity', width: 12 },
|
||||
];
|
||||
|
||||
// Заполнение данными
|
||||
(data as ItemWithRelations[]).forEach((item) => {
|
||||
worksheet.addRow({
|
||||
location: item.category?.location?.name || 'Без локации',
|
||||
category: item.category?.name || 'Без категории',
|
||||
code: item.code || '',
|
||||
name: item.name,
|
||||
quantity: item.quantity,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Настройка колонок для отчета по коробкам
|
||||
worksheet.columns = [
|
||||
{ header: 'Локация', key: 'location', width: 15 },
|
||||
{ header: 'Категория', key: 'category', width: 20 },
|
||||
{ header: 'Коробка', key: 'box', width: 20 },
|
||||
{ header: 'Товар', key: 'item', width: 30 },
|
||||
{ header: 'Количество', key: 'quantity', width: 12 },
|
||||
];
|
||||
|
||||
// Заполнение данными
|
||||
(data as BoxWithRelations[]).forEach((box) => {
|
||||
box.items.forEach((item) => {
|
||||
worksheet.addRow({
|
||||
location: box.category?.location?.name || 'Без локации',
|
||||
category: box.category?.name || 'Без категории',
|
||||
box: box.name,
|
||||
item: item.item.name,
|
||||
quantity: item.quantity,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Стилизация заголовков
|
||||
const headerRow = worksheet.getRow(1);
|
||||
headerRow.font = { bold: true };
|
||||
headerRow.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFE0E0E0' },
|
||||
};
|
||||
headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||
|
||||
return await workbook.xlsx.writeBuffer() as Buffer;
|
||||
}
|
||||
|
||||
static async generatePDFReport(
|
||||
data: ItemWithRelations[] | BoxWithRelations[],
|
||||
type: 'inventory' | 'boxes'
|
||||
): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const doc = new PDFDocument({ margin: 50 });
|
||||
|
||||
doc.on('data', (chunk) => chunks.push(chunk));
|
||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
doc.on('error', reject);
|
||||
|
||||
// Добавляем заголовок
|
||||
doc
|
||||
.fontSize(16)
|
||||
.text(
|
||||
type === 'inventory' ? 'Инвентарный отчет' : 'Отчет по коробкам',
|
||||
{ align: 'center' }
|
||||
);
|
||||
doc.moveDown();
|
||||
|
||||
if (type === 'inventory') {
|
||||
// Группируем данные по локациям и категориям
|
||||
const grouped = (data as ItemWithRelations[]).reduce(
|
||||
(acc, item) => {
|
||||
const locationName = item.category?.location?.name || 'Без локации';
|
||||
const categoryName = item.category?.name || 'Без категории';
|
||||
|
||||
if (!acc[locationName]) {
|
||||
acc[locationName] = {};
|
||||
}
|
||||
if (!acc[locationName][categoryName]) {
|
||||
acc[locationName][categoryName] = [];
|
||||
}
|
||||
|
||||
acc[locationName][categoryName].push(item);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Record<string, ItemWithRelations[]>>
|
||||
);
|
||||
|
||||
// Выводим данные
|
||||
Object.entries(grouped).forEach(([location, categories]) => {
|
||||
doc.fontSize(14).text(location, { underline: true });
|
||||
doc.moveDown(0.5);
|
||||
|
||||
Object.entries(categories).forEach(([category, items]) => {
|
||||
doc.font('Helvetica-Bold').fontSize(12).text(category);
|
||||
doc.moveDown(0.5);
|
||||
|
||||
items.forEach((item) => {
|
||||
doc.fontSize(10).text(
|
||||
`${item.code || 'Без кода'} - ${item.name}: ${item.quantity} шт.`,
|
||||
{ indent: 20 }
|
||||
);
|
||||
});
|
||||
doc.moveDown();
|
||||
});
|
||||
doc.moveDown();
|
||||
});
|
||||
} else {
|
||||
// Вывод данных по коробкам
|
||||
(data as BoxWithRelations[]).forEach((box) => {
|
||||
doc
|
||||
.fontSize(12)
|
||||
.text(
|
||||
`${box.category?.location?.name || 'Без локации'} - ${
|
||||
box.category?.name || 'Без категории'
|
||||
}`,
|
||||
{ underline: true }
|
||||
);
|
||||
doc.fontSize(11).text(`Коробка: ${box.name}`);
|
||||
doc.moveDown(0.5);
|
||||
|
||||
box.items.forEach((item) => {
|
||||
doc.fontSize(10).text(`${item.item.name}: ${item.quantity} шт.`, { indent: 20 });
|
||||
});
|
||||
doc.moveDown();
|
||||
});
|
||||
}
|
||||
|
||||
doc.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ReportService;
|
|
@ -0,0 +1,103 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
import { scheduleJob } from 'node-schedule';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
class YearEndService {
|
||||
// Запускаем планировщик для автоматического переноса
|
||||
static initScheduler() {
|
||||
// Запускаем задачу в последний день года в 23:59
|
||||
scheduleJob('59 23 31 12 *', async () => {
|
||||
try {
|
||||
await YearEndService.transferRemainders();
|
||||
} catch (error) {
|
||||
console.error('Error in year-end transfer:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Метод для ручного запуска переноса остатков
|
||||
static async transferRemainders() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const nextYear = currentYear + 1;
|
||||
|
||||
try {
|
||||
await prisma.$transaction(async (prisma) => {
|
||||
// Получаем все локации
|
||||
const locations = await prisma.location.findMany();
|
||||
|
||||
for (const location of locations) {
|
||||
// Получаем все категории для локации
|
||||
const categories = await prisma.category.findMany({
|
||||
where: { location_id: location.id }
|
||||
});
|
||||
|
||||
for (const category of categories) {
|
||||
// Создаем новую категорию для следующего года
|
||||
const newCategory = await prisma.category.create({
|
||||
data: {
|
||||
name: `${nextYear} Остатки - ${category.name}`,
|
||||
location_id: location.id
|
||||
}
|
||||
});
|
||||
|
||||
// Получаем все товары из текущей категории
|
||||
const items = await prisma.item.findMany({
|
||||
where: { category_id: category.id }
|
||||
});
|
||||
|
||||
// Переносим товары с остатками в новую категорию
|
||||
for (const item of items) {
|
||||
if (item.quantity > 0) {
|
||||
await prisma.item.create({
|
||||
data: {
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: `Перенесено из ${currentYear} года - ${item.description || ''}`,
|
||||
quantity: item.quantity,
|
||||
category_id: newCategory.id
|
||||
}
|
||||
});
|
||||
|
||||
// Обнуляем количество в старой записи
|
||||
await prisma.item.update({
|
||||
where: { id: item.id },
|
||||
data: { quantity: 0 }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем запись в журнале переносов
|
||||
await prisma.yearEndTransfer.create({
|
||||
data: {
|
||||
year: currentYear,
|
||||
completed_at: new Date(),
|
||||
status: 'completed'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`Year-end transfer for ${currentYear} completed successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error during year-end transfer:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Метод для проверки статуса последнего переноса
|
||||
static async getLastTransferStatus() {
|
||||
try {
|
||||
const lastTransfer = await prisma.yearEndTransfer.findFirst({
|
||||
orderBy: { completed_at: 'desc' }
|
||||
});
|
||||
return lastTransfer;
|
||||
} catch (error) {
|
||||
console.error('Error getting last transfer status:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default YearEndService;
|
|
@ -0,0 +1,3 @@
|
|||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"types": ["node"] // Добавлено
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue