Fix: estimates geo v2

This commit is contained in:
Arsen
2026-02-04 00:11:19 +05:00
commit 3f0086f88e
22567 changed files with 4348823 additions and 0 deletions

161
README.md Executable file
View File

@@ -0,0 +1,161 @@
# Смета Ассистент
Веб-приложение для автоматизации создания смет на изыскательские работы с ИИ-ассистентом.
## Возможности
- **Чат с ИИ**: Извлечение данных из текста ТЗ или PDF файлов
- **Конструктор смет**: Визуальный редактор позиций сметы
- **Справочники базовых цен**: СБЦ геодезия, геология, экология
- **Автоматический расчёт**: Коэффициенты, перевод в текущие цены, НДС
- **Экспорт в PDF**: Генерация готовой сметы (при наличии Python — через модуль на ReportLab с корректной кириллицей)
## Структура проекта
```
├── backend/ # Node.js + Express + Prisma
│ ├── prisma/
│ │ ├── schema.prisma # Схема БД
│ │ └── seed.ts # Начальные данные
│ └── src/
│ ├── routes/ # API роуты
│ └── services/ # Бизнес-логика
├── frontend/ # React + Vite + Tailwind
│ └── src/
│ ├── components/ # React компоненты
│ └── api/ # API клиент
└── data/
└── price-books/ # JSON справочники цен
```
## Требования
- Node.js 18+
- PostgreSQL 14+
- npm или yarn
## Установка
### 1. База данных
Создайте базу данных PostgreSQL:
```sql
CREATE DATABASE estimate_assistant;
```
### 2. Backend
Для корректной кириллицы в PDF рекомендуется использовать **Python-модуль** (ReportLab): установите Python 3.8+, в папке `backend/pdf_generator` выполните `pip install -r requirements.txt`. На Windows скрипт использует системный Arial; иначе положите TTF в `backend/pdf_generator/fonts/` (см. `backend/pdf_generator/README.md`). Если Python недоступен, backend использует генерацию на Node (нужен `npm install` в `backend` для шрифтов). После изменений перезапустите backend (`npm run dev`).
```bash
cd backend
# Установка зависимостей
npm install
# Скопируйте .env.example в .env и настройте
cp .env.example .env
# Создание таблиц в БД
npm run prisma:push
# Загрузка начальных данных
npm run prisma:seed
# Запуск сервера (порт 5000)
npm run dev
```
### 3. Frontend
```bash
cd frontend
# Установка зависимостей
npm install
# Запуск (порт 3000)
npm run dev
```
## Конфигурация (.env)
```env
# База данных
DATABASE_URL="postgresql://it:iiEasy348ax@@localhost:5432/estimate_assistant?schema=public"
# Сервер
PORT=5000
# AI провайдер: iieasy или lmstudio
AI_PROVIDER=iieasy
# ai.iieasy.ru
IIEASY_API_URL=https://ai.iieasy.ru/v1
IIEASY_API_KEY=your-api-key
IIEASY_MODEL=google/gemma-3n-e4b
# LM Studio (локальный)
LMSTUDIO_API_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=local-model
# Настройки по умолчанию
DEFAULT_EXECUTOR=ООО "ГеоВектор"
DEFAULT_VAT_RATE=20
DEFAULT_COMPANY_COEF=0.2092
```
## API
### Сметы
- `GET /api/estimates` - Список смет
- `POST /api/estimates` - Создать смету
- `GET /api/estimates/:id` - Получить смету
- `PUT /api/estimates/:id` - Обновить смету
- `POST /api/estimates/:id/items` - Добавить позицию
- `POST /api/estimates/:id/recalculate` - Пересчитать
- `GET /api/estimates/:id/pdf` - Скачать PDF
### Справочники
- `GET /api/price-books` - Список справочников
- `GET /api/price-books/items/search?query=...` - Поиск позиций
- `GET /api/price-books/coefficients/all` - Коэффициенты
- `GET /api/price-books/directions/all` - Направления изысканий
### Чат
- `POST /api/chat/sessions` - Создать сессию
- `POST /api/chat/sessions/:id/messages` - Отправить сообщение
- `POST /api/chat/sessions/:id/upload` - Загрузить файл
### Настройки
- `GET /api/settings` - Получить настройки
- `PUT /api/settings/:key` - Обновить настройку
### Администрирование
- `GET /api/admin/stats` - Статистика БД
- `POST /api/admin/price-books/import-json` - Импорт справочника
## Справочники базовых цен
Данные извлечены из официальных СБЦ:
- СБЦ "Инженерно-геодезические изыскания" (2004)
- СБЦ "Инженерно-геологические и инженерно-экологические изыскания" (1999)
JSON файлы находятся в `data/price-books/`.
## Формула расчёта сметы
```
Базовая_стоимость = Цена_СБЦ × Объём × К1 × К2 × К3
Итого_изыскания = Сумма_работ + Транспорт + Орг_ликв
С_районным_коэф = Итого × Районный_коэф
Текущиеены = С_районным_коэф × Инфляционный_индекс
Итого_без_НДС = Текущиеены × Коэф_компании × Коэф_исполнителя
НДС = Итого_без_НДС × Ставка_НДС
Всего = Итого_без_НДС + НДС
```
## Лицензия
MIT

25
backend/.env Executable file
View File

@@ -0,0 +1,25 @@
# Database
DATABASE_URL="postgresql://its:Nemo348ax@@localhost:5432/estimate_assistant?schema=public"
# Server
PORT=5000
NODE_ENV=development
JWT_SECRET=change-this-in-production-to-random-string
# AI Providers
AI_PROVIDER=iieasy
# iieasy | lmstudio
# iieasy.ru API
IIEASY_API_URL=https://ai.iieasy.ru/v1
IIEASY_API_KEY=your-api-key
IIEASY_MODEL=google/gemma-3n-e4b
# LM Studio (local)
LMSTUDIO_API_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=local-model
# Default company settings
DEFAULT_EXECUTOR=ООО "ГеоВектор"
DEFAULT_VAT_RATE=20
DEFAULT_COMPANY_COEF=0.2092

24
backend/.env.example Executable file
View File

@@ -0,0 +1,24 @@
# Database
DATABASE_URL="postgresql://it:iiEasy348ax@@localhost:5432/estimate_assistant?schema=public"
# Server
PORT=5000
NODE_ENV=development
# AI Providers
AI_PROVIDER=iieasy
# iieasy | lmstudio
# iieasy.ru API
IIEASY_API_URL=https://ai.iieasy.ru/v1
IIEASY_API_KEY=your-api-key
IIEASY_MODEL=google/gemma-3n-e4b
# LM Studio (local)
LMSTUDIO_API_URL=http://localhost:1234/v1
LMSTUDIO_MODEL=local-model
# Default company settings
DEFAULT_EXECUTOR=ООО "ГеоВектор"
DEFAULT_VAT_RATE=20
DEFAULT_COMPANY_COEF=0.2092

4
backend/dist/index.d.ts vendored Executable file
View File

@@ -0,0 +1,4 @@
import { PrismaClient } from '@prisma/client';
declare const prisma: PrismaClient<import(".prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
export { prisma };
//# sourceMappingURL=index.d.ts.map

1
backend/dist/index.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAc9C,QAAA,MAAM,MAAM,gIAAqB,CAAC;AA2ClC,OAAO,EAAE,MAAM,EAAE,CAAC"}

58
backend/dist/index.js vendored Executable file
View File

@@ -0,0 +1,58 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.prisma = void 0;
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const dotenv_1 = __importDefault(require("dotenv"));
const client_1 = require("@prisma/client");
// Routes
const auth_1 = __importDefault(require("./routes/auth"));
const estimates_1 = __importDefault(require("./routes/estimates"));
const priceBooks_1 = __importDefault(require("./routes/priceBooks"));
const chat_1 = __importDefault(require("./routes/chat"));
const settings_1 = __importDefault(require("./routes/settings"));
const admin_1 = __importDefault(require("./routes/admin"));
const auth_2 = require("./middleware/auth");
dotenv_1.default.config();
const app = (0, express_1.default)();
const prisma = new client_1.PrismaClient();
exports.prisma = prisma;
const PORT = process.env.PORT || 5000;
// Middleware — фронт на :3500, прокси Vite тоже
app.use((0, cors_1.default)({ origin: ['http://localhost:3500', 'http://127.0.0.1:3500'], credentials: true }));
app.use((0, cookie_parser_1.default)());
app.use(express_1.default.json({ limit: '50mb' }));
app.use(express_1.default.urlencoded({ extended: true, limit: '50mb' }));
// Health check (без авторизации)
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Auth (публичные)
app.use('/api/auth', auth_1.default);
// Защищённые маршруты
app.use('/api/estimates', auth_2.requireAuth, estimates_1.default);
app.use('/api/price-books', auth_2.requireAuth, priceBooks_1.default);
app.use('/api/chat', auth_2.requireAuth, chat_1.default);
app.use('/api/settings', auth_2.requireAuth, settings_1.default);
app.use('/api/admin', auth_2.requireAuth, admin_1.default);
// Error handler
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ error: err.message || 'Internal server error' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`API available at http://localhost:${PORT}/api`);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down...');
await prisma.$disconnect();
process.exit(0);
});
//# sourceMappingURL=index.js.map

1
backend/dist/index.js.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

11
backend/dist/middleware/auth.d.ts vendored Executable file
View File

@@ -0,0 +1,11 @@
import { Request, Response, NextFunction } from 'express';
export interface JwtPayload {
userId: string;
email: string;
}
export interface AuthRequest extends Request {
user?: JwtPayload;
}
export declare function requireAuth(req: AuthRequest, res: Response, next: NextFunction): Response<any, Record<string, any>> | undefined;
export declare function getTokenPayload(token: string): JwtPayload | null;
//# sourceMappingURL=auth.d.ts.map

1
backend/dist/middleware/auth.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK1D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC1C,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,kDAkB9E;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAMhE"}

35
backend/dist/middleware/auth.js vendored Executable file
View File

@@ -0,0 +1,35 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireAuth = requireAuth;
exports.getTokenPayload = getTokenPayload;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
function requireAuth(req, res, next) {
const token = req.cookies?.token ||
(req.headers.authorization?.startsWith('Bearer ')
? req.headers.authorization.slice(7)
: null);
if (!token) {
return res.status(401).json({ error: 'Требуется авторизация' });
}
try {
const decoded = jsonwebtoken_1.default.verify(token, JWT_SECRET);
req.user = { userId: decoded.userId, email: decoded.email };
next();
}
catch {
return res.status(401).json({ error: 'Недействительный или истёкший токен' });
}
}
function getTokenPayload(token) {
try {
return jsonwebtoken_1.default.verify(token, JWT_SECRET);
}
catch {
return null;
}
}
//# sourceMappingURL=auth.js.map

1
backend/dist/middleware/auth.js.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":";;;;;AAcA,kCAkBC;AAED,0CAMC;AAvCD,gEAA+B;AAE/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iCAAiC,CAAC;AAW/E,SAAgB,WAAW,CAAC,GAAgB,EAAE,GAAa,EAAE,IAAkB;IAC7E,MAAM,KAAK,GACT,GAAG,CAAC,OAAO,EAAE,KAAK;QAClB,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,UAAU,CAAC,SAAS,CAAC;YAC/C,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;YACpC,CAAC,CAAC,IAAI,CAAC,CAAC;IAEZ,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAe,CAAC;QAC5D,GAAG,CAAC,IAAI,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5D,IAAI,EAAE,CAAC;IACT,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAC,CAAC;IAChF,CAAC;AACH,CAAC;AAED,SAAgB,eAAe,CAAC,KAAa;IAC3C,IAAI,CAAC;QACH,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAe,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}

3
backend/dist/routes/admin.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=admin.d.ts.map

1
backend/dist/routes/admin.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2SxB,eAAe,MAAM,CAAC"}

299
backend/dist/routes/admin.js vendored Executable file
View File

@@ -0,0 +1,299 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const multer_1 = __importDefault(require("multer"));
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
// Configure multer for file uploads
const upload = (0, multer_1.default)({
storage: multer_1.default.memoryStorage(),
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
fileFilter: (req, file, cb) => {
if (file.mimetype === 'application/pdf' || file.mimetype === 'application/json') {
cb(null, true);
}
else {
cb(new Error('Only PDF and JSON files are allowed'));
}
},
});
// Get all price books
router.get('/price-books', async (req, res) => {
try {
const priceBooks = await prisma.priceBook.findMany({
include: {
_count: {
select: { tables: true, items: true },
},
},
orderBy: { createdAt: 'desc' },
});
res.json(priceBooks);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch price books' });
}
});
// Import price book from JSON
router.post('/price-books/import-json', upload.single('file'), async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const data = JSON.parse(file.buffer.toString('utf-8'));
// Validate structure
if (!data.priceBook || !data.tables) {
return res.status(400).json({ error: 'Invalid JSON structure' });
}
// Check if already exists
const existing = await prisma.priceBook.findUnique({
where: { code: data.priceBook.code },
});
if (existing) {
return res.status(400).json({ error: `Price book ${data.priceBook.code} already exists` });
}
// Create price book
const priceBook = await prisma.priceBook.create({
data: {
code: data.priceBook.code,
name: data.priceBook.name,
baseDate: new Date(data.priceBook.baseDate),
approvedBy: data.priceBook.approvedBy,
effectiveDate: data.priceBook.effectiveDate ? new Date(data.priceBook.effectiveDate) : null,
},
});
let tablesCount = 0;
let itemsCount = 0;
// Create tables and items
for (const table of data.tables) {
const priceTable = await prisma.priceTable.create({
data: {
priceBookId: priceBook.id,
tableNumber: table.tableNumber,
name: table.name,
unit: table.unit,
notes: table.notes || null,
},
});
tablesCount++;
for (const item of table.items) {
await prisma.priceItem.create({
data: {
priceBookId: priceBook.id,
priceTableId: priceTable.id,
paragraph: item.paragraph,
workType: item.workType || item.networkType || item.type || `Таблица ${table.tableNumber}`,
description: item.description || null,
priceField1: item.category1Field ?? item.cat1 ?? item.undevelopedField ?? null,
priceOffice1: item.category1Office ?? item.undevelopedOffice ?? null,
priceField2: item.category2Field ?? item.cat2 ?? item.builtUpField ?? null,
priceOffice2: item.category2Office ?? item.builtUpOffice ?? null,
priceField3: item.category3Field ?? item.cat3 ?? item.industrialField ?? null,
priceOffice3: item.category3Office ?? item.industrialOffice ?? null,
priceSimple: item.price ?? item.fieldPrice ?? null,
attributes: item,
},
});
itemsCount++;
}
}
res.json({
success: true,
priceBook: {
id: priceBook.id,
code: priceBook.code,
name: priceBook.name,
},
imported: {
tables: tablesCount,
items: itemsCount,
},
});
}
catch (error) {
console.error('Import error:', error);
res.status(500).json({ error: error.message || 'Failed to import price book' });
}
});
// Delete price book
router.delete('/price-books/:id', async (req, res) => {
try {
await prisma.priceBook.delete({
where: { id: req.params.id },
});
res.status(204).send();
}
catch (error) {
res.status(400).json({ error: 'Failed to delete price book' });
}
});
// Get all coefficients (for editor, including inactive)
router.get('/coefficients', async (req, res) => {
try {
const { type } = req.query;
const where = {};
if (type)
where.type = String(type);
const coefficients = await prisma.coefficient.findMany({
where,
orderBy: [{ type: 'asc' }, { code: 'asc' }],
});
res.json(coefficients);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch coefficients' });
}
});
// Add coefficient
router.post('/coefficients', async (req, res) => {
try {
const { type, code, name, value, description, conditions } = req.body;
const coefficient = await prisma.coefficient.create({
data: {
type,
code,
name,
value: new client_1.Prisma.Decimal(value),
description,
conditions,
},
});
res.status(201).json(coefficient);
}
catch (error) {
res.status(400).json({ error: error.message || 'Failed to create coefficient' });
}
});
// Update coefficient
router.put('/coefficients/:id', async (req, res) => {
try {
const { name, value, description, conditions, isActive } = req.body;
const coefficient = await prisma.coefficient.update({
where: { id: req.params.id },
data: {
name,
value: value ? new client_1.Prisma.Decimal(value) : undefined,
description,
conditions,
isActive,
},
});
res.json(coefficient);
}
catch (error) {
res.status(400).json({ error: error.message || 'Failed to update coefficient' });
}
});
// Delete coefficient
router.delete('/coefficients/:id', async (req, res) => {
try {
await prisma.coefficient.delete({
where: { id: req.params.id },
});
res.status(204).send();
}
catch (error) {
res.status(400).json({ error: 'Failed to delete coefficient' });
}
});
// Get all inflation indices (for editor, including inactive)
router.get('/inflation-indices', async (req, res) => {
try {
const indices = await prisma.inflationIndex.findMany({
orderBy: { effectiveFrom: 'desc' },
});
res.json(indices);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch inflation indices' });
}
});
// Add inflation index
router.post('/inflation-indices', async (req, res) => {
try {
const { baseDate, effectiveFrom, effectiveTo, indexValue, documentRef } = req.body;
const index = await prisma.inflationIndex.create({
data: {
baseDate: new Date(baseDate),
effectiveFrom: new Date(effectiveFrom),
effectiveTo: effectiveTo ? new Date(effectiveTo) : null,
indexValue: new client_1.Prisma.Decimal(indexValue),
documentRef,
},
});
res.status(201).json(index);
}
catch (error) {
res.status(400).json({ error: error.message || 'Failed to create inflation index' });
}
});
// Update inflation index
router.put('/inflation-indices/:id', async (req, res) => {
try {
const { baseDate, effectiveFrom, effectiveTo, indexValue, documentRef, isActive } = req.body;
const data = {};
if (baseDate != null)
data.baseDate = new Date(baseDate);
if (effectiveFrom != null)
data.effectiveFrom = new Date(effectiveFrom);
if (effectiveTo !== undefined)
data.effectiveTo = effectiveTo ? new Date(effectiveTo) : null;
if (indexValue != null)
data.indexValue = new client_1.Prisma.Decimal(indexValue);
if (documentRef !== undefined)
data.documentRef = documentRef;
if (isActive !== undefined)
data.isActive = isActive;
const index = await prisma.inflationIndex.update({
where: { id: req.params.id },
data,
});
res.json(index);
}
catch (error) {
res.status(400).json({ error: error.message || 'Failed to update inflation index' });
}
});
// Delete inflation index
router.delete('/inflation-indices/:id', async (req, res) => {
try {
await prisma.inflationIndex.delete({
where: { id: req.params.id },
});
res.status(204).send();
}
catch (error) {
res.status(400).json({ error: 'Failed to delete inflation index' });
}
});
// Database stats
router.get('/stats', async (req, res) => {
try {
const [priceBooks, tables, items, coefficients, estimates, sessions] = await Promise.all([
prisma.priceBook.count(),
prisma.priceTable.count(),
prisma.priceItem.count(),
prisma.coefficient.count(),
prisma.estimate.count(),
prisma.chatSession.count(),
]);
res.json({
priceBooks,
tables,
items,
coefficients,
estimates,
sessions,
});
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch stats' });
}
});
exports.default = router;
//# sourceMappingURL=admin.js.map

1
backend/dist/routes/admin.js.map vendored Executable file

File diff suppressed because one or more lines are too long

3
backend/dist/routes/auth.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=auth.d.ts.map

1
backend/dist/routes/auth.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0IxB,eAAe,MAAM,CAAC"}

132
backend/dist/routes/auth.js vendored Executable file
View File

@@ -0,0 +1,132 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const bcrypt_1 = __importDefault(require("bcrypt"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
const COOKIE_OPTIONS = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};
function signToken(userId, email) {
return jsonwebtoken_1.default.sign({ userId, email }, JWT_SECRET, { expiresIn: '7d' });
}
function setTokenCookie(res, token) {
res.cookie('token', token, COOKIE_OPTIONS);
}
// POST /api/auth/register
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Укажите email и пароль' });
}
const normalizedEmail = String(email).trim().toLowerCase();
if (normalizedEmail.length < 3) {
return res.status(400).json({ error: 'Некорректный email' });
}
if (String(password).length < 4) {
return res.status(400).json({ error: 'Пароль не менее 4 символов' });
}
const existing = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
if (existing) {
return res.status(400).json({ error: 'Пользователь с таким email уже зарегистрирован' });
}
const passwordHash = await bcrypt_1.default.hash(password, 10);
const user = await prisma.user.create({
data: {
email: normalizedEmail,
passwordHash,
name: name ? String(name).trim() || null : null,
},
});
const token = signToken(user.id, user.email);
setTokenCookie(res, token);
res.status(201).json({
user: { id: user.id, email: user.email, name: user.name },
token,
});
}
catch (error) {
console.error('Register error:', error);
res.status(500).json({ error: 'Ошибка регистрации' });
}
});
// POST /api/auth/login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Укажите email и пароль' });
}
const normalizedEmail = String(email).trim().toLowerCase();
const user = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
if (!user) {
return res.status(401).json({ error: 'Неверный email или пароль' });
}
const valid = await bcrypt_1.default.compare(password, user.passwordHash);
if (!valid) {
return res.status(401).json({ error: 'Неверный email или пароль' });
}
const token = signToken(user.id, user.email);
setTokenCookie(res, token);
res.json({
user: { id: user.id, email: user.email, name: user.name },
token,
});
}
catch (error) {
console.error('Login error:', error);
const message = process.env.NODE_ENV === 'development' && error?.message
? error.message
: 'Ошибка входа';
res.status(500).json({ error: message });
}
});
// POST /api/auth/logout
router.post('/logout', (_req, res) => {
res.clearCookie('token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
});
res.json({ ok: true });
});
// GET /api/auth/me (requires auth)
router.get('/me', async (req, res) => {
const token = req.cookies?.token ||
(req.headers.authorization?.startsWith('Bearer ')
? req.headers.authorization.slice(7)
: null);
if (!token) {
return res.status(401).json({ error: 'Требуется авторизация' });
}
try {
const decoded = jsonwebtoken_1.default.verify(token, JWT_SECRET);
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, name: true },
});
if (!user) {
return res.status(401).json({ error: 'Пользователь не найден' });
}
res.json({ user });
}
catch {
return res.status(401).json({ error: 'Недействительный токен' });
}
});
exports.default = router;
//# sourceMappingURL=auth.js.map

1
backend/dist/routes/auth.js.map vendored Executable file

File diff suppressed because one or more lines are too long

3
backend/dist/routes/chat.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=chat.d.ts.map

1
backend/dist/routes/chat.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiSxB,eAAe,MAAM,CAAC"}

337
backend/dist/routes/chat.js vendored Executable file
View File

@@ -0,0 +1,337 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const multer_1 = __importDefault(require("multer"));
const XLSX = __importStar(require("xlsx"));
const ai_service_1 = require("../services/ai.service");
const parser_service_1 = require("../services/parser.service");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
function getUserId(req) {
if (!req.user?.userId)
throw new Error('Unauthorized');
return req.user.userId;
}
const aiService = new ai_service_1.AIService();
const parserService = new parser_service_1.ParserService(prisma, aiService);
const EXCEL_MIMES = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-excel', // .xls
];
function isExcelFile(mimetype) {
return EXCEL_MIMES.includes(mimetype);
}
function parseExcelToText(buffer) {
try {
const workbook = XLSX.read(buffer, { type: 'buffer' });
const lines = [];
for (const sheetName of workbook.SheetNames) {
const sheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
lines.push(`=== Лист: ${sheetName} ===`);
for (const row of data) {
if (row && row.length > 0) {
const rowStr = row
.map(cell => (cell != null ? String(cell).trim() : ''))
.filter(Boolean)
.join(' | ');
if (rowStr)
lines.push(rowStr);
}
}
}
return lines.join('\n');
}
catch (err) {
console.error('Excel parse error:', err);
return '[Не удалось прочитать Excel файл]';
}
}
// Configure multer for file uploads
const upload = (0, multer_1.default)({
storage: multer_1.default.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
fileFilter: (req, file, cb) => {
if (file.mimetype === 'application/pdf' ||
file.mimetype.startsWith('text/') ||
isExcelFile(file.mimetype)) {
cb(null, true);
}
else {
cb(new Error('Разрешены только PDF, TXT и Excel (.xlsx, .xls)'));
}
},
});
// Create new chat session (user's own context)
router.post('/sessions', async (req, res) => {
try {
const userId = getUserId(req);
const session = await prisma.chatSession.create({
data: {
userId,
estimateId: req.body.estimateId || null,
},
});
res.status(201).json(session);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to create chat session' });
}
});
// Get chat session by estimate ID (current user's session for this estimate)
router.get('/sessions/by-estimate/:estimateId', async (req, res) => {
try {
const userId = getUserId(req);
const session = await prisma.chatSession.findFirst({
where: { estimateId: req.params.estimateId, userId },
include: {
messages: { orderBy: { createdAt: 'asc' } },
},
});
if (!session) {
return res.status(404).json({ error: 'Session not found for this estimate' });
}
res.json(session);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to fetch session' });
}
});
// Update chat session (e.g. link to estimate after creation); only owner
router.patch('/sessions/:id', async (req, res) => {
try {
const userId = getUserId(req);
const existing = await prisma.chatSession.findUnique({
where: { id: req.params.id },
});
if (!existing)
return res.status(404).json({ error: 'Session not found' });
if (existing.userId !== userId)
return res.status(403).json({ error: 'Access denied' });
const data = 'estimateId' in req.body ? { estimateId: req.body.estimateId || null } : {};
const session = await prisma.chatSession.update({
where: { id: req.params.id },
data,
});
res.json(session);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: 'Failed to update session' });
}
});
// Get chat session with messages; only owner
router.get('/sessions/:id', async (req, res) => {
try {
const userId = getUserId(req);
const session = await prisma.chatSession.findUnique({
where: { id: req.params.id },
include: {
messages: { orderBy: { createdAt: 'asc' } },
},
});
if (!session)
return res.status(404).json({ error: 'Session not found' });
if (session.userId !== userId)
return res.status(403).json({ error: 'Access denied' });
res.json(session);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to fetch session' });
}
});
// Send message to chat
router.post('/sessions/:id/messages', async (req, res) => {
try {
const userId = getUserId(req);
const { content } = req.body;
const sessionId = req.params.id;
const session = await prisma.chatSession.findUnique({
where: { id: sessionId },
});
if (!session)
return res.status(404).json({ error: 'Session not found' });
if (session.userId !== userId)
return res.status(403).json({ error: 'Access denied' });
// Save user message
const userMessage = await prisma.chatMessage.create({
data: {
sessionId,
role: 'user',
content,
},
});
// Get conversation history
const history = await prisma.chatMessage.findMany({
where: { sessionId },
orderBy: { createdAt: 'asc' },
take: 20, // Last 20 messages for context
});
// Process with AI and parser (with estimate context)
const response = await parserService.processMessage(content, history, session.estimateId);
// Save assistant message
const assistantMessage = await prisma.chatMessage.create({
data: {
sessionId,
role: 'assistant',
content: response.message,
metadata: response.extractedData ? JSON.parse(JSON.stringify(response.extractedData)) : null,
},
});
res.json({
userMessage,
assistantMessage,
extractedData: response.extractedData,
needsClarification: response.needsClarification,
clarificationQuestions: response.clarificationQuestions,
});
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
console.error('Chat error:', error);
res.status(500).json({ error: error.message || 'Failed to process message' });
}
});
// Upload PDF/TZ file
router.post('/sessions/:id/upload', upload.single('file'), async (req, res) => {
try {
const userId = getUserId(req);
const sessionId = req.params.id;
const file = req.file;
if (!file)
return res.status(400).json({ error: 'No file uploaded' });
const session = await prisma.chatSession.findUnique({
where: { id: sessionId },
});
if (!session)
return res.status(404).json({ error: 'Session not found' });
if (session.userId !== userId)
return res.status(403).json({ error: 'Access denied' });
// Extract text from file
let textContent = '';
if (file.mimetype === 'application/pdf') {
textContent = `[Содержимое PDF файла: ${file.originalname}]`;
}
else if (isExcelFile(file.mimetype)) {
textContent = parseExcelToText(file.buffer);
if (!textContent.trim()) {
textContent = `[Excel файл пуст или не удалось прочитать: ${file.originalname}]`;
}
else {
textContent = `Загружена смета/данные из Excel (${file.originalname}):\n\n${textContent}`;
}
}
else {
textContent = file.buffer.toString('utf-8');
}
// Save file upload as user message
const userMessage = await prisma.chatMessage.create({
data: {
sessionId,
role: 'user',
content: `Загружен файл: ${file.originalname}\n\n${textContent}`,
metadata: {
fileType: file.mimetype,
fileName: file.originalname,
fileSize: file.size,
},
},
});
// Process with AI
const history = await prisma.chatMessage.findMany({
where: { sessionId },
orderBy: { createdAt: 'asc' },
take: 10,
});
const response = await parserService.processMessage(textContent, history, session.estimateId);
// Save assistant response
const assistantMessage = await prisma.chatMessage.create({
data: {
sessionId,
role: 'assistant',
content: response.message,
metadata: response.extractedData ? JSON.parse(JSON.stringify(response.extractedData)) : null,
},
});
res.json({
userMessage,
assistantMessage,
extractedData: response.extractedData,
needsClarification: response.needsClarification,
clarificationQuestions: response.clarificationQuestions,
});
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
console.error('Upload error:', error);
res.status(500).json({ error: error.message || 'Failed to process file' });
}
});
// Delete chat session (only owner)
router.delete('/sessions/:id', async (req, res) => {
try {
const userId = getUserId(req);
const session = await prisma.chatSession.findUnique({
where: { id: req.params.id },
});
if (!session)
return res.status(404).json({ error: 'Session not found' });
if (session.userId !== userId)
return res.status(403).json({ error: 'Access denied' });
await prisma.chatSession.delete({ where: { id: req.params.id } });
res.status(204).send();
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: 'Failed to delete session' });
}
});
exports.default = router;
//# sourceMappingURL=chat.js.map

1
backend/dist/routes/chat.js.map vendored Executable file

File diff suppressed because one or more lines are too long

3
backend/dist/routes/estimates.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=estimates.d.ts.map

1
backend/dist/routes/estimates.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"estimates.d.ts","sourceRoot":"","sources":["../../src/routes/estimates.ts"],"names":[],"mappings":"AAMA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA0TxB,eAAe,MAAM,CAAC"}

337
backend/dist/routes/estimates.js vendored Executable file
View File

@@ -0,0 +1,337 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const estimate_service_1 = require("../services/estimate.service");
const pdf_service_1 = require("../services/pdf.service");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
const estimateService = new estimate_service_1.EstimateService(prisma);
const pdfService = new pdf_service_1.PdfService();
function getUserId(req) {
if (!req.user?.userId)
throw new Error('Unauthorized');
return req.user.userId;
}
async function canAccessEstimate(estimateId, userId) {
const estimate = await prisma.estimate.findUnique({
where: { id: estimateId },
select: { ownerId: true },
});
if (!estimate)
return false;
if (estimate.ownerId === userId)
return true;
const share = await prisma.estimateShare.findUnique({
where: { estimateId_sharedWithId: { estimateId, sharedWithId: userId } },
});
return !!share;
}
async function isOwner(estimateId, userId) {
const estimate = await prisma.estimate.findUnique({
where: { id: estimateId },
select: { ownerId: true },
});
return estimate?.ownerId === userId;
}
// Get all estimates (owned + shared with me), with sharedWithMe flag
router.get('/', async (req, res) => {
try {
const userId = getUserId(req);
const owned = await prisma.estimate.findMany({
where: { ownerId: userId },
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
orderBy: { createdAt: 'desc' },
});
const shared = await prisma.estimateShare.findMany({
where: { sharedWithId: userId },
include: {
estimate: {
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
},
},
});
const sharedEstimates = shared.map(s => ({
...s.estimate,
sharedWithMe: true,
}));
const ownedWithFlag = owned.map(e => ({ ...e, sharedWithMe: false }));
const combined = [...ownedWithFlag, ...sharedEstimates].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
res.json(combined);
}
catch (error) {
if (error.message === 'Unauthorized') {
return res.status(401).json({ error: error.message });
}
res.status(500).json({ error: 'Failed to fetch estimates' });
}
});
// Get estimate by ID (owner or shared)
router.get('/:id', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok) {
return res.status(404).json({ error: 'Estimate not found' });
}
const estimate = await prisma.estimate.findUnique({
where: { id: req.params.id },
include: {
direction: true,
items: {
orderBy: { orderNumber: 'asc' },
include: { priceItem: true },
},
totals: { orderBy: { orderNumber: 'asc' } },
},
});
if (!estimate) {
return res.status(404).json({ error: 'Estimate not found' });
}
const sharedWithMe = estimate.ownerId !== userId;
res.json({ ...estimate, sharedWithMe });
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to fetch estimate' });
}
});
// Create new estimate
router.post('/', async (req, res) => {
try {
const userId = getUserId(req);
const estimate = await estimateService.createEstimate({
...req.body,
ownerId: userId,
});
res.status(201).json(estimate);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to create estimate' });
}
});
// Update estimate (owner only)
router.put('/:id', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await isOwner(req.params.id, userId);
if (!ok) {
return res.status(403).json({ error: 'Только владелец может изменять смету' });
}
const estimate = await estimateService.updateEstimate(req.params.id, req.body);
res.json(estimate);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to update estimate' });
}
});
// Add item (owner or shared)
router.post('/:id/items', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok)
return res.status(404).json({ error: 'Estimate not found' });
const item = await estimateService.addEstimateItem(req.params.id, req.body);
res.status(201).json(item);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to add item' });
}
});
// Update estimate item (owner or shared)
router.put('/:id/items/:itemId', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok)
return res.status(404).json({ error: 'Estimate not found' });
const item = await estimateService.updateEstimateItem(req.params.itemId, req.body);
res.json(item);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to update item' });
}
});
// Delete estimate item (owner or shared)
router.delete('/:id/items/:itemId', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok)
return res.status(404).json({ error: 'Estimate not found' });
await prisma.estimateItem.delete({ where: { id: req.params.itemId } });
res.status(204).send();
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: 'Failed to delete item' });
}
});
// Recalculate (owner or shared)
router.post('/:id/recalculate', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok)
return res.status(404).json({ error: 'Estimate not found' });
const estimate = await estimateService.recalculateTotals(req.params.id);
res.json(estimate);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to recalculate' });
}
});
// Generate PDF (owner or shared)
router.get('/:id/pdf', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await canAccessEstimate(req.params.id, userId);
if (!ok)
return res.status(404).json({ error: 'Estimate not found' });
const estimate = await prisma.estimate.findUnique({
where: { id: req.params.id },
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
});
if (!estimate) {
return res.status(404).json({ error: 'Estimate not found' });
}
const pdfBuffer = await pdfService.generateEstimatePdf(estimate);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="smeta-${estimate.number}.pdf"`);
res.send(pdfBuffer);
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to generate PDF' });
}
});
// Delete estimate (owner only)
router.delete('/:id', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await isOwner(req.params.id, userId);
if (!ok) {
return res.status(403).json({ error: 'Только владелец может удалить смету' });
}
await prisma.estimate.delete({ where: { id: req.params.id } });
res.status(204).send();
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: 'Failed to delete estimate' });
}
});
// Share estimate with user by email
router.post('/:id/share', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await isOwner(req.params.id, userId);
if (!ok) {
return res.status(403).json({ error: 'Только владелец может поделиться сметой' });
}
const { email } = req.body;
if (!email || typeof email !== 'string') {
return res.status(400).json({ error: 'Укажите email пользователя' });
}
const normalizedEmail = String(email).trim().toLowerCase();
const sharedWith = await prisma.user.findUnique({
where: { email: normalizedEmail },
});
if (!sharedWith) {
return res.status(404).json({ error: 'Пользователь с таким email не найден' });
}
if (sharedWith.id === userId) {
return res.status(400).json({ error: 'Нельзя поделиться сметой с самим собой' });
}
await prisma.estimateShare.upsert({
where: {
estimateId_sharedWithId: { estimateId: req.params.id, sharedWithId: sharedWith.id },
},
create: {
estimateId: req.params.id,
ownerId: userId,
sharedWithId: sharedWith.id,
},
update: {},
});
res.status(201).json({ sharedWith: { id: sharedWith.id, email: sharedWith.email, name: sharedWith.name } });
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: error.message || 'Failed to share' });
}
});
// Unshare: remove share for a user
router.delete('/:id/share/:sharedWithUserId', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await isOwner(req.params.id, userId);
if (!ok) {
return res.status(403).json({ error: 'Только владелец может отменить доступ' });
}
await prisma.estimateShare.deleteMany({
where: {
estimateId: req.params.id,
sharedWithId: req.params.sharedWithUserId,
ownerId: userId,
},
});
res.status(204).send();
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(400).json({ error: 'Failed to unshare' });
}
});
// List users this estimate is shared with
router.get('/:id/shares', async (req, res) => {
try {
const userId = getUserId(req);
const ok = await isOwner(req.params.id, userId);
if (!ok)
return res.status(403).json({ error: 'Доступ запрещён' });
const shares = await prisma.estimateShare.findMany({
where: { estimateId: req.params.id, ownerId: userId },
include: {
sharedWith: { select: { id: true, email: true, name: true } },
},
});
res.json(shares.map(s => ({ id: s.id, sharedWith: s.sharedWith, createdAt: s.createdAt })));
}
catch (error) {
if (error.message === 'Unauthorized')
return res.status(401).json({ error: error.message });
res.status(500).json({ error: 'Failed to fetch shares' });
}
});
exports.default = router;
//# sourceMappingURL=estimates.js.map

1
backend/dist/routes/estimates.js.map vendored Executable file

File diff suppressed because one or more lines are too long

3
backend/dist/routes/priceBooks.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=priceBooks.d.ts.map

1
backend/dist/routes/priceBooks.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"priceBooks.d.ts","sourceRoot":"","sources":["../../src/routes/priceBooks.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAsLxB,eAAe,MAAM,CAAC"}

181
backend/dist/routes/priceBooks.js vendored Executable file
View File

@@ -0,0 +1,181 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
// Get all price books
router.get('/', async (req, res) => {
try {
const priceBooks = await prisma.priceBook.findMany({
where: { isActive: true },
include: {
tables: {
orderBy: { tableNumber: 'asc' },
},
},
orderBy: { code: 'asc' },
});
res.json(priceBooks);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch price books' });
}
});
// Get price book by ID
router.get('/:id', async (req, res) => {
try {
const priceBook = await prisma.priceBook.findUnique({
where: { id: req.params.id },
include: {
tables: {
orderBy: { tableNumber: 'asc' },
include: {
items: {
orderBy: { paragraph: 'asc' },
},
},
},
},
});
if (!priceBook) {
return res.status(404).json({ error: 'Price book not found' });
}
res.json(priceBook);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch price book' });
}
});
// Get price table by ID
router.get('/tables/:tableId', async (req, res) => {
try {
const table = await prisma.priceTable.findUnique({
where: { id: req.params.tableId },
include: {
items: { orderBy: { paragraph: 'asc' } },
priceBook: true,
},
});
if (!table) {
return res.status(404).json({ error: 'Price table not found' });
}
res.json(table);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch price table' });
}
});
// Маппинг направления изысканий на код справочника СБЦ
const DIRECTION_TO_PRICE_BOOK_CODE = {
geodesy: 'SBC-GEODESY-2004',
geology: 'SBC-GEOLOGY-1999',
ecology: 'SBC-GEOLOGY-1999',
hydrology: 'SBC-HYDROLOGY-2001',
};
// Search price items (поддержка directionCode для выбора наименований из СБЦ)
router.get('/items/search', async (req, res) => {
try {
const { query, priceBookId, directionCode, tableNumber, limit = 50 } = req.query;
const where = {};
if (query) {
where.OR = [
{ workType: { contains: String(query), mode: 'insensitive' } },
{ paragraph: { contains: String(query), mode: 'insensitive' } },
{ description: { contains: String(query), mode: 'insensitive' } },
];
}
if (priceBookId) {
where.priceBookId = String(priceBookId);
}
else if (directionCode) {
const code = DIRECTION_TO_PRICE_BOOK_CODE[String(directionCode)];
if (code) {
const book = await prisma.priceBook.findFirst({ where: { code } });
if (book)
where.priceBookId = book.id;
}
}
if (tableNumber) {
where.priceTable = { tableNumber: Number(tableNumber) };
}
const items = await prisma.priceItem.findMany({
where,
include: {
priceBook: { select: { code: true, name: true } },
priceTable: { select: { tableNumber: true, name: true, unit: true } },
},
take: Number(limit),
orderBy: { paragraph: 'asc' },
});
res.json(items);
}
catch (error) {
res.status(500).json({ error: 'Failed to search price items' });
}
});
// Get price item by ID
router.get('/items/:itemId', async (req, res) => {
try {
const item = await prisma.priceItem.findUnique({
where: { id: req.params.itemId },
include: {
priceBook: true,
priceTable: true,
},
});
if (!item) {
return res.status(404).json({ error: 'Price item not found' });
}
res.json(item);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch price item' });
}
});
// Get all coefficients
router.get('/coefficients/all', async (req, res) => {
try {
const { type } = req.query;
const where = { isActive: true };
if (type) {
where.type = String(type);
}
const coefficients = await prisma.coefficient.findMany({
where,
orderBy: [{ type: 'asc' }, { code: 'asc' }],
});
res.json(coefficients);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch coefficients' });
}
});
// Get inflation indices
router.get('/inflation-indices', async (req, res) => {
try {
const indices = await prisma.inflationIndex.findMany({
where: { isActive: true },
orderBy: { effectiveFrom: 'desc' },
});
res.json(indices);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch inflation indices' });
}
});
// Get survey directions
router.get('/directions/all', async (req, res) => {
try {
const directions = await prisma.surveyDirection.findMany({
where: { isActive: true },
orderBy: { name: 'asc' },
});
res.json(directions);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch survey directions' });
}
});
exports.default = router;
//# sourceMappingURL=priceBooks.js.map

1
backend/dist/routes/priceBooks.js.map vendored Executable file

File diff suppressed because one or more lines are too long

3
backend/dist/routes/settings.d.ts vendored Executable file
View File

@@ -0,0 +1,3 @@
declare const router: import("express-serve-static-core").Router;
export default router;
//# sourceMappingURL=settings.d.ts.map

1
backend/dist/routes/settings.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/routes/settings.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAiHxB,eAAe,MAAM,CAAC"}

112
backend/dist/routes/settings.js vendored Executable file
View File

@@ -0,0 +1,112 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
// Get all settings
router.get('/', async (req, res) => {
try {
const { category } = req.query;
const where = {};
if (category) {
where.category = String(category);
}
const settings = await prisma.setting.findMany({
where,
orderBy: [{ category: 'asc' }, { key: 'asc' }],
});
// Convert to key-value object grouped by category
const grouped = settings.reduce((acc, setting) => {
if (!acc[setting.category]) {
acc[setting.category] = {};
}
let value = setting.value;
if (setting.type === 'number') {
value = parseFloat(setting.value);
}
else if (setting.type === 'boolean') {
value = setting.value === 'true';
}
else if (setting.type === 'json') {
try {
value = JSON.parse(setting.value);
}
catch { }
}
acc[setting.category][setting.key] = {
value,
label: setting.label,
type: setting.type,
};
return acc;
}, {});
res.json(grouped);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch settings' });
}
});
// Get setting by key
router.get('/:key', async (req, res) => {
try {
const setting = await prisma.setting.findUnique({
where: { key: req.params.key },
});
if (!setting) {
return res.status(404).json({ error: 'Setting not found' });
}
res.json(setting);
}
catch (error) {
res.status(500).json({ error: 'Failed to fetch setting' });
}
});
// Update setting
router.put('/:key', async (req, res) => {
try {
const { value } = req.body;
const setting = await prisma.setting.update({
where: { key: req.params.key },
data: {
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
},
});
res.json(setting);
}
catch (error) {
res.status(400).json({ error: 'Failed to update setting' });
}
});
// Create or update multiple settings
router.post('/batch', async (req, res) => {
try {
const { settings } = req.body;
const results = [];
for (const [key, data] of Object.entries(settings)) {
const result = await prisma.setting.upsert({
where: { key },
update: {
value: typeof data.value === 'object' ? JSON.stringify(data.value) : String(data.value),
label: data.label,
type: data.type,
category: data.category,
},
create: {
key,
value: typeof data.value === 'object' ? JSON.stringify(data.value) : String(data.value),
label: data.label || key,
type: data.type || 'string',
category: data.category || 'general',
},
});
results.push(result);
}
res.json(results);
}
catch (error) {
res.status(400).json({ error: 'Failed to update settings' });
}
});
exports.default = router;
//# sourceMappingURL=settings.js.map

1
backend/dist/routes/settings.js.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/routes/settings.ts"],"names":[],"mappings":"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"}

69
backend/dist/services/ai.service.d.ts vendored Executable file
View File

@@ -0,0 +1,69 @@
interface ChatMessage {
role: 'user' | 'assistant' | 'system';
content: string;
}
interface AIResponse {
content: string;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
export declare class AIService {
private provider;
private iieasyUrl;
private iieasyKey;
private iieasyModel;
private lmstudioUrl;
private lmstudioModel;
constructor();
setProvider(provider: 'iieasy' | 'lmstudio'): void;
chat(messages: ChatMessage[], systemPrompt?: string): Promise<AIResponse>;
private chatIIEasy;
private chatLMStudio;
extractEstimateData(text: string, previousData?: Partial<{
direction: string;
customer: string;
objectName: string;
executor: string;
works: Array<{
name: string;
volume: number;
unit: string;
}>;
}>, estimateContext?: {
objectName: string;
customer: string;
executor?: string;
direction?: string;
items?: Array<{
workName: string;
quantity: number;
unit?: string;
}>;
}): Promise<any>;
discussEstimate(userMessage: string, history: Array<{
role: string;
content: string;
}>, estimateJson: {
objectName: string;
customer: string;
executor?: string;
direction?: string;
items?: Array<{
workName: string;
quantity: number;
unit?: string;
basePrice?: number;
totalPrice?: number;
}>;
totals?: Array<{
label: string;
resultValue: number;
}>;
}): Promise<string>;
findPriceItems(workDescription: string, priceItems: any[]): Promise<any[]>;
}
export {};
//# sourceMappingURL=ai.service.d.ts.map

1
backend/dist/services/ai.service.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"ai.service.d.ts","sourceRoot":"","sources":["../../src/services/ai.service.ts"],"names":[],"mappings":"AAGA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,UAAU;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE;QACN,aAAa,EAAE,MAAM,CAAC;QACtB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,aAAa,CAAS;;IAW9B,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU;IAIrC,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;YAgBjE,UAAU;YAiCV,YAAY;IAgCpB,mBAAmB,CACvB,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,OAAO,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC9D,CAAC,EACF,eAAe,CAAC,EAAE;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtE,GACA,OAAO,CAAC,GAAG,CAAC;IAkET,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,EACjD,YAAY,EAAE;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC9G,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACxD,GACA,OAAO,CAAC,MAAM,CAAC;IAkCZ,cAAc,CAAC,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;CA8BjF"}

205
backend/dist/services/ai.service.js vendored Executable file
View File

@@ -0,0 +1,205 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIService = void 0;
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
class AIService {
constructor() {
this.provider = process.env.AI_PROVIDER || 'iieasy';
this.iieasyUrl = process.env.IIEASY_API_URL || 'https://ai.iieasy.ru/v1';
this.iieasyKey = process.env.IIEASY_API_KEY || '';
this.iieasyModel = process.env.IIEASY_MODEL || 'google/gemma-3n-e4b';
this.lmstudioUrl = process.env.LMSTUDIO_API_URL || 'http://localhost:1234/v1';
this.lmstudioModel = process.env.LMSTUDIO_MODEL || 'local-model';
}
setProvider(provider) {
this.provider = provider;
}
async chat(messages, systemPrompt) {
const allMessages = [];
if (systemPrompt) {
allMessages.push({ role: 'system', content: systemPrompt });
}
allMessages.push(...messages);
if (this.provider === 'lmstudio') {
return this.chatLMStudio(allMessages);
}
else {
return this.chatIIEasy(allMessages);
}
}
async chatIIEasy(messages) {
try {
const response = await fetch(`${this.iieasyUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.iieasyKey}`,
},
body: JSON.stringify({
model: this.iieasyModel,
messages,
temperature: 0.7,
max_tokens: 4096,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`IIEasy API error: ${response.status} - ${error}`);
}
const data = await response.json();
return {
content: data.choices[0]?.message?.content || '',
usage: data.usage,
};
}
catch (error) {
console.error('IIEasy API error:', error);
throw new Error(`AI service error: ${error.message}`);
}
}
async chatLMStudio(messages) {
try {
const response = await fetch(`${this.lmstudioUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: this.lmstudioModel,
messages,
temperature: 0.7,
max_tokens: 4096,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`LM Studio API error: ${response.status} - ${error}`);
}
const data = await response.json();
return {
content: data.choices[0]?.message?.content || '',
usage: data.usage,
};
}
catch (error) {
console.error('LM Studio API error:', error);
throw new Error(`AI service error: ${error.message}`);
}
}
async extractEstimateData(text, previousData, estimateContext) {
const prevHints = [];
if (previousData?.customer?.trim())
prevHints.push(`Заказчик: "${previousData.customer}" — сохрани в customer, если в тексте не указан другой.`);
if (previousData?.direction?.trim())
prevHints.push(`Направление: "${previousData.direction}" — сохрани в direction, если пользователь не указал другое.`);
if (previousData?.objectName?.trim())
prevHints.push(`Объект: "${previousData.objectName}" — сохрани в objectName, если пользователь не указал другой.`);
if (previousData?.executor?.trim())
prevHints.push(`Исполнитель: "${previousData.executor}" — сохрани в executor, если не указан другой.`);
if (previousData?.works?.length)
prevHints.push(`Работы уже указаны — добавляй только новые, либо объединяй с ними.`);
const previousDataHint = prevHints.length
? `\nРанее в беседе указано:\n${prevHints.join('\n')}\nВ этих полях НЕ пиши "не указан" — используй ранее указанное значение или опусти поле.\n`
: '';
const estimateContextHint = estimateContext
? `\nТекущая смета (контекст беседы):\n- Объект: ${estimateContext.objectName}\n- Заказчик: ${estimateContext.customer}\n${estimateContext.executor ? `- Исполнитель: ${estimateContext.executor}\n` : ''}${estimateContext.direction ? `- Направление: ${estimateContext.direction}\n` : ''}${estimateContext.items && estimateContext.items.length > 0 ? `- Позиции сметы: ${estimateContext.items.map(i => `${i.workName} (${i.quantity} ${i.unit || 'шт.'})`).join(', ')}\n` : ''}\nОтвечай в контексте этой сметы. Пользователь может просить добавить работы, уточнить данные или задавать вопросы о смете.\n`
: '';
const systemPrompt = `Ты - ассистент для составления смет на изыскательские работы.
Проанализируй текст и извлеки следующую информацию в JSON:
- direction: направление изысканий — ОДНО из: geodesy, geology, ecology, hydrology (не перечень работ).
- customer: заказчик (организация или ФИО).
- objectName: полное наименование ОБЪЕКТА изысканий/строительства — это название объекта, НЕ перечень работ.
Примеры правильного objectName: «АО «Святогор». Месторождение «Волковское». Третья очередь. Комплекс объектов инфраструктуры обогатительной фабрики и открытого рудника», «Строительство школы в г. Москва».
НИКОГДА не подставляй в objectName: "работы", "перечень работ", названия видов работ (топосъёмка, нивелирование и т.п.) — это только для поля works.
- works: список работ с объёмами (name — краткое описание работы для подбора по СБЦ, volume — число, unit — единица: га, км, шт. и т.д.).
${previousDataHint}
${estimateContextHint}
Отвечай ТОЛЬКО в формате JSON, без дополнительного текста.
Пример ответа:
{
"direction": "geodesy",
"customer": "ООО Компания",
"objectName": "АО «Святогор». Месторождение «Волковское». Третья очередь. Комплекс объектов инфраструктуры обогатительной фабрики и открытого рудника",
"works": [
{"name": "Топографическая съемка", "volume": 10, "unit": "га"},
{"name": "Нивелирование", "volume": 5, "unit": "км"}
]
}`;
const response = await this.chat([{ role: 'user', content: text }], systemPrompt);
try {
// Try to parse JSON from response
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
throw new Error('No JSON found in response');
}
catch (error) {
console.error('Failed to parse AI response:', response.content);
return {
error: true,
rawResponse: response.content,
};
}
}
async discussEstimate(userMessage, history, estimateJson) {
const itemsStr = estimateJson.items && estimateJson.items.length > 0
? estimateJson.items.map((i, idx) => `${idx + 1}. ${i.workName}: ${i.quantity} ${i.unit || 'шт.'}${i.totalPrice != null ? i.totalPrice + ' руб.' : ''}`).join('\n')
: 'Нет позиций';
const totalsStr = estimateJson.totals && estimateJson.totals.length > 0
? estimateJson.totals.map(t => `${t.label}: ${t.resultValue} руб.`).join('\n')
: '';
const systemPrompt = `Ты — эксперт-сметчик по изыскательским работам. Пользователь обсуждает смету. Отвечай на русском.
Текущая смета:
- Объект: ${estimateJson.objectName}
- Заказчик: ${estimateJson.customer}
${estimateJson.executor ? `- Исполнитель: ${estimateJson.executor}\n` : ''}${estimateJson.direction ? `- Направление: ${estimateJson.direction}\n` : ''}
Позиции:
${itemsStr}
${totalsStr ? `\nИтоги:\n${totalsStr}` : ''}
Помогай с рекомендациями, оптимизацией, корректировкой позиций и объёмов. Отвечай кратко и по делу.`;
const messages = [
...history.slice(-10).map(m => ({
role: m.role,
content: m.content,
})),
{ role: 'user', content: userMessage },
].filter(m => m.role !== 'system');
const response = await this.chat(messages, systemPrompt);
return response.content;
}
async findPriceItems(workDescription, priceItems) {
const systemPrompt = `Ты - эксперт по сметному делу в изысканиях.
Тебе даны: описание работы и список позиций из справочника базовых цен.
Найди наиболее подходящие позиции для данной работы.
Отвечай ТОЛЬКО в формате JSON - массив индексов подходящих позиций.
Пример: [0, 2, 5]
Если ничего не подходит, верни пустой массив: []`;
const itemsList = priceItems.map((item, idx) => `${idx}. ${item.paragraph}: ${item.workType}`).join('\n');
const response = await this.chat([{ role: 'user', content: `Работа: ${workDescription}\n\nПозиции справочника:\n${itemsList}` }], systemPrompt);
try {
const jsonMatch = response.content.match(/\[[\s\S]*\]/);
if (jsonMatch) {
const indices = JSON.parse(jsonMatch[0]);
return indices.map((idx) => priceItems[idx]).filter(Boolean);
}
return [];
}
catch (error) {
return [];
}
}
}
exports.AIService = AIService;
//# sourceMappingURL=ai.service.js.map

1
backend/dist/services/ai.service.js.map vendored Executable file

File diff suppressed because one or more lines are too long

258
backend/dist/services/estimate.service.d.ts vendored Executable file
View File

@@ -0,0 +1,258 @@
import { PrismaClient, Prisma } from '@prisma/client';
export declare class EstimateService {
private prisma;
constructor(prisma: PrismaClient);
createEstimate(data: {
ownerId: string;
directionCode: string;
objectName: string;
customer: string;
executor?: string;
vatRate?: number;
}): Promise<{
direction: {
name: string;
id: string;
code: string;
shortName: string;
isActive: boolean;
};
} & {
number: string;
id: string;
createdAt: Date;
updatedAt: Date;
ownerId: string;
objectName: string;
customer: string;
executor: string;
totalFieldWorks: Prisma.Decimal | null;
totalOfficeWorks: Prisma.Decimal | null;
totalLaboratory: Prisma.Decimal | null;
subtotal: Prisma.Decimal | null;
regionalCoef: Prisma.Decimal | null;
inflationIndex: Prisma.Decimal | null;
inflationDocRef: string | null;
companyCoef: Prisma.Decimal | null;
executorCoef: Prisma.Decimal | null;
totalWithoutVat: Prisma.Decimal | null;
vatRate: Prisma.Decimal | null;
vatAmount: Prisma.Decimal | null;
totalWithVat: Prisma.Decimal | null;
status: string;
directionId: string;
}>;
updateEstimate(id: string, data: Partial<{
objectName: string;
customer: string;
executor: string;
regionalCoef: number;
inflationIndex: number;
inflationDocRef: string;
companyCoef: number;
executorCoef: number;
vatRate: number;
status: string;
}>): Promise<{
direction: {
name: string;
id: string;
code: string;
shortName: string;
isActive: boolean;
};
items: {
id: string;
createdAt: Date;
updatedAt: Date;
orderNumber: number;
estimateId: string;
sectionType: string;
priceItemId: string | null;
workName: string;
justification: string | null;
basePrice: Prisma.Decimal;
quantity: Prisma.Decimal;
unit: string | null;
coef1: Prisma.Decimal | null;
coef1Desc: string | null;
coef2: Prisma.Decimal | null;
coef2Desc: string | null;
coef3: Prisma.Decimal | null;
coef3Desc: string | null;
totalPrice: Prisma.Decimal;
}[];
totals: {
id: string;
createdAt: Date;
updatedAt: Date;
label: string;
orderNumber: number;
estimateId: string;
description: string | null;
baseValue: Prisma.Decimal | null;
coefficient: Prisma.Decimal | null;
resultValue: Prisma.Decimal;
}[];
} & {
number: string;
id: string;
createdAt: Date;
updatedAt: Date;
ownerId: string;
objectName: string;
customer: string;
executor: string;
totalFieldWorks: Prisma.Decimal | null;
totalOfficeWorks: Prisma.Decimal | null;
totalLaboratory: Prisma.Decimal | null;
subtotal: Prisma.Decimal | null;
regionalCoef: Prisma.Decimal | null;
inflationIndex: Prisma.Decimal | null;
inflationDocRef: string | null;
companyCoef: Prisma.Decimal | null;
executorCoef: Prisma.Decimal | null;
totalWithoutVat: Prisma.Decimal | null;
vatRate: Prisma.Decimal | null;
vatAmount: Prisma.Decimal | null;
totalWithVat: Prisma.Decimal | null;
status: string;
directionId: string;
}>;
addEstimateItem(estimateId: string, data: {
sectionType: string;
priceItemId?: string;
workName: string;
justification?: string;
basePrice: number;
quantity: number;
unit?: string;
coef1?: number;
coef1Desc?: string;
coef2?: number;
coef2Desc?: string;
coef3?: number;
coef3Desc?: string;
}): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
orderNumber: number;
estimateId: string;
sectionType: string;
priceItemId: string | null;
workName: string;
justification: string | null;
basePrice: Prisma.Decimal;
quantity: Prisma.Decimal;
unit: string | null;
coef1: Prisma.Decimal | null;
coef1Desc: string | null;
coef2: Prisma.Decimal | null;
coef2Desc: string | null;
coef3: Prisma.Decimal | null;
coef3Desc: string | null;
totalPrice: Prisma.Decimal;
}>;
updateEstimateItem(itemId: string, data: Partial<{
workName: string;
justification: string;
basePrice: number;
quantity: number;
unit: string;
coef1: number;
coef1Desc: string;
coef2: number;
coef2Desc: string;
coef3: number;
coef3Desc: string;
}>): Promise<{
id: string;
createdAt: Date;
updatedAt: Date;
orderNumber: number;
estimateId: string;
sectionType: string;
priceItemId: string | null;
workName: string;
justification: string | null;
basePrice: Prisma.Decimal;
quantity: Prisma.Decimal;
unit: string | null;
coef1: Prisma.Decimal | null;
coef1Desc: string | null;
coef2: Prisma.Decimal | null;
coef2Desc: string | null;
coef3: Prisma.Decimal | null;
coef3Desc: string | null;
totalPrice: Prisma.Decimal;
}>;
recalculateTotals(estimateId: string): Promise<{
direction: {
name: string;
id: string;
code: string;
shortName: string;
isActive: boolean;
};
items: {
id: string;
createdAt: Date;
updatedAt: Date;
orderNumber: number;
estimateId: string;
sectionType: string;
priceItemId: string | null;
workName: string;
justification: string | null;
basePrice: Prisma.Decimal;
quantity: Prisma.Decimal;
unit: string | null;
coef1: Prisma.Decimal | null;
coef1Desc: string | null;
coef2: Prisma.Decimal | null;
coef2Desc: string | null;
coef3: Prisma.Decimal | null;
coef3Desc: string | null;
totalPrice: Prisma.Decimal;
}[];
totals: {
id: string;
createdAt: Date;
updatedAt: Date;
label: string;
orderNumber: number;
estimateId: string;
description: string | null;
baseValue: Prisma.Decimal | null;
coefficient: Prisma.Decimal | null;
resultValue: Prisma.Decimal;
}[];
} & {
number: string;
id: string;
createdAt: Date;
updatedAt: Date;
ownerId: string;
objectName: string;
customer: string;
executor: string;
totalFieldWorks: Prisma.Decimal | null;
totalOfficeWorks: Prisma.Decimal | null;
totalLaboratory: Prisma.Decimal | null;
subtotal: Prisma.Decimal | null;
regionalCoef: Prisma.Decimal | null;
inflationIndex: Prisma.Decimal | null;
inflationDocRef: string | null;
companyCoef: Prisma.Decimal | null;
executorCoef: Prisma.Decimal | null;
totalWithoutVat: Prisma.Decimal | null;
vatRate: Prisma.Decimal | null;
vatAmount: Prisma.Decimal | null;
totalWithVat: Prisma.Decimal | null;
status: string;
directionId: string;
}>;
private updateTotalsSection;
}
//# sourceMappingURL=estimate.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"estimate.service.d.ts","sourceRoot":"","sources":["../../src/services/estimate.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEtD,qBAAa,eAAe;IACd,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,YAAY;IAElC,cAAc,CAAC,IAAI,EAAE;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4CK,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;QAC7C,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2BI,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE;QAC9C,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;;;;;;;;;;;;;;;;IA0CK,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;QACrD,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;;;;;;;;;;;;;;;;;;;;;IAyCI,iBAAiB,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAqE5B,mBAAmB;CAqGlC"}

317
backend/dist/services/estimate.service.js vendored Executable file
View File

@@ -0,0 +1,317 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EstimateService = void 0;
const client_1 = require("@prisma/client");
class EstimateService {
constructor(prisma) {
this.prisma = prisma;
}
async createEstimate(data) {
// Get direction
const direction = await this.prisma.surveyDirection.findUnique({
where: { code: data.directionCode },
});
if (!direction) {
throw new Error(`Direction not found: ${data.directionCode}`);
}
// Get default executor from settings
let executor = data.executor;
if (!executor) {
const defaultExecutor = await this.prisma.setting.findUnique({
where: { key: 'default_executor' },
});
executor = defaultExecutor?.value || 'Не указан';
}
// Generate estimate number (per owner)
const count = await this.prisma.estimate.count({
where: { ownerId: data.ownerId },
});
const number = `${count + 1}`;
const estimate = await this.prisma.estimate.create({
data: {
number,
directionId: direction.id,
ownerId: data.ownerId,
objectName: data.objectName,
customer: data.customer,
executor,
vatRate: data.vatRate ? new client_1.Prisma.Decimal(data.vatRate) : new client_1.Prisma.Decimal(20),
status: 'draft',
},
include: {
direction: true,
},
});
return estimate;
}
async updateEstimate(id, data) {
const updateData = {};
if (data.objectName)
updateData.objectName = data.objectName;
if (data.customer)
updateData.customer = data.customer;
if (data.executor)
updateData.executor = data.executor;
if (data.regionalCoef !== undefined)
updateData.regionalCoef = new client_1.Prisma.Decimal(data.regionalCoef);
if (data.inflationIndex !== undefined)
updateData.inflationIndex = new client_1.Prisma.Decimal(data.inflationIndex);
if (data.inflationDocRef)
updateData.inflationDocRef = data.inflationDocRef;
if (data.companyCoef !== undefined)
updateData.companyCoef = new client_1.Prisma.Decimal(data.companyCoef);
if (data.executorCoef !== undefined)
updateData.executorCoef = new client_1.Prisma.Decimal(data.executorCoef);
if (data.vatRate !== undefined)
updateData.vatRate = new client_1.Prisma.Decimal(data.vatRate);
if (data.status)
updateData.status = data.status;
const estimate = await this.prisma.estimate.update({
where: { id },
data: updateData,
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
});
return estimate;
}
async addEstimateItem(estimateId, data) {
// Get max order number
const maxOrder = await this.prisma.estimateItem.aggregate({
where: { estimateId },
_max: { orderNumber: true },
});
const orderNumber = (maxOrder._max.orderNumber || 0) + 1;
// Calculate total price
const basePrice = new client_1.Prisma.Decimal(data.basePrice);
const quantity = new client_1.Prisma.Decimal(data.quantity);
let totalPrice = basePrice.mul(quantity);
if (data.coef1)
totalPrice = totalPrice.mul(new client_1.Prisma.Decimal(data.coef1));
if (data.coef2)
totalPrice = totalPrice.mul(new client_1.Prisma.Decimal(data.coef2));
if (data.coef3)
totalPrice = totalPrice.mul(new client_1.Prisma.Decimal(data.coef3));
const item = await this.prisma.estimateItem.create({
data: {
estimateId,
orderNumber,
sectionType: data.sectionType,
priceItemId: data.priceItemId || null,
workName: data.workName,
justification: data.justification || null,
basePrice,
quantity,
unit: data.unit || null,
coef1: data.coef1 ? new client_1.Prisma.Decimal(data.coef1) : null,
coef1Desc: data.coef1Desc || null,
coef2: data.coef2 ? new client_1.Prisma.Decimal(data.coef2) : null,
coef2Desc: data.coef2Desc || null,
coef3: data.coef3 ? new client_1.Prisma.Decimal(data.coef3) : null,
coef3Desc: data.coef3Desc || null,
totalPrice,
},
});
return item;
}
async updateEstimateItem(itemId, data) {
const updateData = {};
if (data.workName)
updateData.workName = data.workName;
if (data.justification)
updateData.justification = data.justification;
if (data.basePrice !== undefined)
updateData.basePrice = new client_1.Prisma.Decimal(data.basePrice);
if (data.quantity !== undefined)
updateData.quantity = new client_1.Prisma.Decimal(data.quantity);
if (data.unit)
updateData.unit = data.unit;
if (data.coef1 !== undefined)
updateData.coef1 = data.coef1 ? new client_1.Prisma.Decimal(data.coef1) : null;
if (data.coef1Desc !== undefined)
updateData.coef1Desc = data.coef1Desc;
if (data.coef2 !== undefined)
updateData.coef2 = data.coef2 ? new client_1.Prisma.Decimal(data.coef2) : null;
if (data.coef2Desc !== undefined)
updateData.coef2Desc = data.coef2Desc;
if (data.coef3 !== undefined)
updateData.coef3 = data.coef3 ? new client_1.Prisma.Decimal(data.coef3) : null;
if (data.coef3Desc !== undefined)
updateData.coef3Desc = data.coef3Desc;
// Recalculate total if price/quantity/coefs changed
const currentItem = await this.prisma.estimateItem.findUnique({ where: { id: itemId } });
if (currentItem) {
const basePrice = updateData.basePrice || currentItem.basePrice;
const quantity = updateData.quantity || currentItem.quantity;
let totalPrice = basePrice.mul(quantity);
const coef1 = updateData.coef1 !== undefined ? updateData.coef1 : currentItem.coef1;
const coef2 = updateData.coef2 !== undefined ? updateData.coef2 : currentItem.coef2;
const coef3 = updateData.coef3 !== undefined ? updateData.coef3 : currentItem.coef3;
if (coef1)
totalPrice = totalPrice.mul(coef1);
if (coef2)
totalPrice = totalPrice.mul(coef2);
if (coef3)
totalPrice = totalPrice.mul(coef3);
updateData.totalPrice = totalPrice;
}
const item = await this.prisma.estimateItem.update({
where: { id: itemId },
data: updateData,
});
return item;
}
async recalculateTotals(estimateId) {
const estimate = await this.prisma.estimate.findUnique({
where: { id: estimateId },
include: {
items: true,
},
});
if (!estimate) {
throw new Error('Estimate not found');
}
// Calculate section totals
const fieldWorks = estimate.items
.filter(i => i.sectionType === 'field')
.reduce((sum, i) => sum.add(i.totalPrice), new client_1.Prisma.Decimal(0));
const officeWorks = estimate.items
.filter(i => i.sectionType === 'office')
.reduce((sum, i) => sum.add(i.totalPrice), new client_1.Prisma.Decimal(0));
const laboratory = estimate.items
.filter(i => i.sectionType === 'laboratory')
.reduce((sum, i) => sum.add(i.totalPrice), new client_1.Prisma.Decimal(0));
const subtotal = estimate.items
.reduce((sum, i) => sum.add(i.totalPrice), new client_1.Prisma.Decimal(0));
// Apply coefficients
const regionalCoef = estimate.regionalCoef || new client_1.Prisma.Decimal(1);
const inflationIndex = estimate.inflationIndex || new client_1.Prisma.Decimal(1);
const companyCoef = estimate.companyCoef || new client_1.Prisma.Decimal(1);
const executorCoef = estimate.executorCoef || new client_1.Prisma.Decimal(1);
let totalWithoutVat = subtotal
.mul(regionalCoef)
.mul(inflationIndex)
.mul(companyCoef)
.mul(executorCoef);
const vatRate = estimate.vatRate || new client_1.Prisma.Decimal(20);
const vatAmount = totalWithoutVat.mul(vatRate).div(100);
const totalWithVat = totalWithoutVat.add(vatAmount);
// Update estimate
const updated = await this.prisma.estimate.update({
where: { id: estimateId },
data: {
totalFieldWorks: fieldWorks,
totalOfficeWorks: officeWorks,
totalLaboratory: laboratory,
subtotal,
totalWithoutVat,
vatAmount,
totalWithVat,
},
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
});
// Update totals section
await this.updateTotalsSection(estimateId, estimate, updated);
return updated;
}
async updateTotalsSection(estimateId, estimate, updated) {
// Delete existing totals
await this.prisma.estimateTotal.deleteMany({ where: { estimateId } });
const totals = [];
let order = 1;
// Add totals rows
if (updated.regionalCoef && Number(updated.regionalCoef) !== 1) {
totals.push({
estimateId,
orderNumber: order++,
label: 'С районным коэффициентом',
description: `К=${updated.regionalCoef}`,
baseValue: updated.subtotal,
coefficient: updated.regionalCoef,
resultValue: updated.subtotal.mul(updated.regionalCoef),
});
}
if (updated.inflationIndex && Number(updated.inflationIndex) !== 1) {
const prevValue = totals.length > 0
? totals[totals.length - 1].resultValue
: updated.subtotal;
totals.push({
estimateId,
orderNumber: order++,
label: 'Перевод в текущие цены',
description: updated.inflationDocRef || `Индекс: ${updated.inflationIndex}`,
baseValue: prevValue,
coefficient: updated.inflationIndex,
resultValue: prevValue.mul(updated.inflationIndex),
});
}
if (updated.companyCoef && Number(updated.companyCoef) !== 1) {
const prevValue = totals.length > 0
? totals[totals.length - 1].resultValue
: updated.subtotal;
totals.push({
estimateId,
orderNumber: order++,
label: 'Коэффициент компании',
description: null,
baseValue: prevValue,
coefficient: updated.companyCoef,
resultValue: prevValue.mul(updated.companyCoef),
});
}
if (updated.executorCoef && Number(updated.executorCoef) !== 1) {
const prevValue = totals.length > 0
? totals[totals.length - 1].resultValue
: updated.subtotal;
totals.push({
estimateId,
orderNumber: order++,
label: `Коэффициент ${estimate.executor}`,
description: null,
baseValue: prevValue,
coefficient: updated.executorCoef,
resultValue: prevValue.mul(updated.executorCoef),
});
}
// Final totals
totals.push({
estimateId,
orderNumber: order++,
label: 'Итого без НДС:',
description: null,
baseValue: null,
coefficient: null,
resultValue: updated.totalWithoutVat,
});
totals.push({
estimateId,
orderNumber: order++,
label: `НДС ${updated.vatRate}%:`,
description: null,
baseValue: updated.totalWithoutVat,
coefficient: updated.vatRate.div(100),
resultValue: updated.vatAmount,
});
totals.push({
estimateId,
orderNumber: order++,
label: 'Всего с НДС:',
description: null,
baseValue: null,
coefficient: null,
resultValue: updated.totalWithVat,
});
// Create totals
for (const total of totals) {
await this.prisma.estimateTotal.create({ data: total });
}
}
}
exports.EstimateService = EstimateService;
//# sourceMappingURL=estimate.service.js.map

File diff suppressed because one or more lines are too long

43
backend/dist/services/parser.service.d.ts vendored Executable file
View File

@@ -0,0 +1,43 @@
import { PrismaClient } from '@prisma/client';
import { AIService } from './ai.service';
interface ParsedData {
direction?: string;
customer?: string;
objectName?: string;
executor?: string;
works?: Array<{
name: string;
volume: number;
unit: string;
priceItemId?: string;
justification?: string;
}>;
vatIncluded?: boolean;
missingFields?: string[];
}
interface ProcessResult {
message: string;
extractedData?: ParsedData;
needsClarification: boolean;
clarificationQuestions?: string[];
}
export declare class ParserService {
private prisma;
private aiService;
constructor(prisma: PrismaClient, aiService: AIService);
/** Режим обсуждения сметы: свободный диалог о прикреплённой смете */
private processEstimateDiscussion;
/** Берём последние извлечённые данные из истории (ответ ассистента с metadata) */
private getPreviousExtractedData;
/** Значения-заполнители: не перезаписываем ими ранее указанные данные */
private static readonly PLACEHOLDER_VALUES;
private isPlaceholder;
/** Объединяем новые данные с предыдущими: не затираем поля, которые пользователь уже указал */
private mergeExtractedData;
processMessage(content: string, history: any[], estimateId?: string | null): Promise<ProcessResult>;
private matchWorksWithPriceItems;
private getRelevantPriceBookCode;
createEstimateFromParsedData(data: ParsedData): Promise<any>;
}
export {};
//# sourceMappingURL=parser.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"parser.service.d.ts","sourceRoot":"","sources":["../../src/services/parser.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,UAAU,CAAC;IAC3B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,qBAAa,aAAa;IAEtB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,SAAS;gBADT,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,SAAS;IAG9B,qEAAqE;YACvD,yBAAyB;IA6CvC,kFAAkF;IAClF,OAAO,CAAC,wBAAwB;IAahC,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAGxC;IAEF,OAAO,CAAC,aAAa;IAMrB,+FAA+F;IAC/F,OAAO,CAAC,kBAAkB;IAgBpB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC;YAyI3F,wBAAwB;IA8BtC,OAAO,CAAC,wBAAwB;IAU1B,4BAA4B,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC;CA8EnE"}

314
backend/dist/services/parser.service.js vendored Executable file
View File

@@ -0,0 +1,314 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParserService = void 0;
class ParserService {
constructor(prisma, aiService) {
this.prisma = prisma;
this.aiService = aiService;
}
/** Режим обсуждения сметы: свободный диалог о прикреплённой смете */
async processEstimateDiscussion(content, history, estimateId) {
const estimate = await this.prisma.estimate.findUnique({
where: { id: estimateId },
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
totals: { orderBy: { orderNumber: 'asc' } },
},
});
if (!estimate)
return null;
const estimateJson = {
objectName: estimate.objectName,
customer: estimate.customer,
executor: estimate.executor || undefined,
direction: estimate.direction?.code,
items: estimate.items.map(i => ({
workName: i.workName,
quantity: Number(i.quantity),
unit: i.unit || undefined,
basePrice: Number(i.basePrice),
totalPrice: Number(i.totalPrice),
})),
totals: estimate.totals?.map(t => ({
label: t.label,
resultValue: Number(t.resultValue),
})),
};
const historyForAi = history.map(m => ({
role: m.role,
content: m.content,
}));
const message = await this.aiService.discussEstimate(content, historyForAi, estimateJson);
return {
message,
needsClarification: false,
};
}
/** Берём последние извлечённые данные из истории (ответ ассистента с metadata) */
getPreviousExtractedData(history) {
for (let i = history.length - 1; i >= 0; i--) {
const msg = history[i];
if (msg.role === 'assistant' && msg.metadata && typeof msg.metadata === 'object') {
const data = msg.metadata;
if (data.direction || data.customer || data.objectName || (data.works && data.works.length > 0)) {
return data;
}
}
}
return null;
}
isPlaceholder(val) {
if (!val || typeof val !== 'string')
return true;
const t = val.trim().toLowerCase();
return ParserService.PLACEHOLDER_VALUES.some(p => t === p.toLowerCase() || t === '');
}
/** Объединяем новые данные с предыдущими: не затираем поля, которые пользователь уже указал */
mergeExtractedData(previous, fresh) {
const take = (freshVal, prevVal) => {
if (freshVal?.trim() && !this.isPlaceholder(freshVal))
return freshVal.trim();
if (prevVal?.trim())
return prevVal;
return undefined;
};
return {
direction: take(fresh.direction, previous?.direction),
customer: take(fresh.customer, previous?.customer),
objectName: take(fresh.objectName, previous?.objectName),
executor: take(fresh.executor, previous?.executor),
works: (fresh.works && fresh.works.length > 0) ? fresh.works : (previous?.works ?? fresh.works),
vatIncluded: fresh.vatIncluded ?? previous?.vatIncluded,
};
}
async processMessage(content, history, estimateId) {
// Режим обсуждения сметы: когда есть прикреплённая смета
if (estimateId) {
const discussionResult = await this.processEstimateDiscussion(content, history, estimateId);
if (discussionResult)
return discussionResult;
}
const previousData = this.getPreviousExtractedData(history);
let estimateContext;
if (estimateId) {
const estimate = await this.prisma.estimate.findUnique({
where: { id: estimateId },
include: {
direction: true,
items: { orderBy: { orderNumber: 'asc' } },
},
});
if (estimate) {
estimateContext = {
objectName: estimate.objectName,
customer: estimate.customer,
executor: estimate.executor || undefined,
direction: estimate.direction?.code,
items: estimate.items.map(i => ({
workName: i.workName,
quantity: Number(i.quantity),
unit: i.unit || undefined,
})),
};
}
}
const freshData = await this.aiService.extractEstimateData(content, previousData ?? undefined, estimateContext);
if (freshData.error) {
return {
message: 'Не удалось обработать сообщение. Пожалуйста, опишите подробнее: какой объект, какие работы нужно выполнить?',
needsClarification: true,
clarificationQuestions: [
'Какое направление изысканий? (геодезия, геология, экология, гидрометеорология)',
'Кто заказчик?',
'Как называется объект?',
'Какие работы и в каком объёме?',
],
};
}
const extractedData = this.mergeExtractedData(previousData, freshData);
// Check for missing fields
const missingFields = [];
const questions = [];
if (!extractedData.direction) {
missingFields.push('direction');
questions.push('Укажите направление изысканий: геодезия, геология, экология или гидрометеорология?');
}
if (!extractedData.customer) {
missingFields.push('customer');
questions.push('Кто заказчик работ?');
}
if (!extractedData.objectName) {
missingFields.push('objectName');
questions.push('Как называется объект? (полное наименование объекта изысканий/строительства)');
}
if (!extractedData.works || extractedData.works.length === 0) {
missingFields.push('works');
questions.push('Какие работы нужно выполнить и в каком объёме?');
}
// If we have enough data, try to match works with price items (СБЦ)
if (extractedData.works && extractedData.works.length > 0) {
await this.matchWorksWithPriceItems(extractedData);
}
// Build response message
let message = '';
if (extractedData.direction || extractedData.customer || extractedData.objectName) {
message += 'Извлечённые данные:\n';
if (extractedData.direction) {
const directionNames = {
geodesy: 'Инженерно-геодезические изыскания',
geology: 'Инженерно-геологические изыскания',
ecology: 'Инженерно-экологические изыскания',
hydrology: 'Инженерно-гидрометеорологические изыскания',
};
message += `- Направление: ${directionNames[extractedData.direction] || extractedData.direction}\n`;
}
if (extractedData.customer) {
message += `- Заказчик: ${extractedData.customer}\n`;
}
if (extractedData.objectName) {
message += `- Объект: ${extractedData.objectName}\n`;
}
if (extractedData.works && extractedData.works.length > 0) {
message += `- Работы (${extractedData.works.length}):\n`;
for (const work of extractedData.works) {
message += `${work.name}: ${work.volume} ${work.unit}`;
if (work.justification) {
message += ` (${work.justification})`;
}
message += '\n';
}
}
}
if (missingFields.length > 0) {
message += '\nДля продолжения уточните:\n';
questions.forEach((q, i) => {
message += `${i + 1}. ${q}\n`;
});
return {
message,
extractedData,
needsClarification: true,
clarificationQuestions: questions,
};
}
// All data is present
message += '\nВсе данные получены. Смета готова к формированию. Создать смету?';
return {
message,
extractedData,
needsClarification: false,
};
}
async matchWorksWithPriceItems(data) {
if (!data.works || !data.direction)
return;
// Get relevant price book
const priceBookCode = this.getRelevantPriceBookCode(data.direction);
const priceBook = await this.prisma.priceBook.findFirst({
where: { code: priceBookCode },
});
if (!priceBook)
return;
// Get all price items for this book
const priceItems = await this.prisma.priceItem.findMany({
where: { priceBookId: priceBook.id },
include: { priceTable: true },
});
// Match each work
for (const work of data.works) {
const matches = await this.aiService.findPriceItems(work.name, priceItems);
if (matches.length > 0) {
const bestMatch = matches[0];
work.priceItemId = bestMatch.id;
work.justification = `${bestMatch.priceTable.name} ${bestMatch.paragraph}`;
}
}
}
getRelevantPriceBookCode(direction) {
const mapping = {
geodesy: 'SBC-GEODESY-2004',
geology: 'SBC-GEOLOGY-1999',
ecology: 'SBC-GEOLOGY-1999',
hydrology: 'SBC-HYDROLOGY-2001',
};
return mapping[direction] || 'SBC-GEODESY-2004';
}
async createEstimateFromParsedData(data) {
if (!data.direction || !data.customer || !data.objectName) {
throw new Error('Missing required fields');
}
// Get direction
const direction = await this.prisma.surveyDirection.findUnique({
where: { code: data.direction },
});
if (!direction) {
throw new Error('Invalid direction');
}
// Get default settings
const executorSetting = await this.prisma.setting.findUnique({
where: { key: 'default_executor' },
});
const vatSetting = await this.prisma.setting.findUnique({
where: { key: 'default_vat_rate' },
});
// Generate estimate number
const count = await this.prisma.estimate.count();
// Create estimate
const estimate = await this.prisma.estimate.create({
data: {
number: `${count + 1}`,
directionId: direction.id,
objectName: data.objectName,
customer: data.customer,
executor: data.executor || executorSetting?.value || 'Не указан',
vatRate: vatSetting ? parseFloat(vatSetting.value) : 20,
status: 'draft',
},
});
// Add works as estimate items; наименование работ — из справочника СБЦ (workType), если есть совпадение
if (data.works && data.works.length > 0) {
for (let i = 0; i < data.works.length; i++) {
const work = data.works[i];
let basePrice = 0;
let workName = work.name;
let unit = work.unit;
if (work.priceItemId) {
const priceItem = await this.prisma.priceItem.findUnique({
where: { id: work.priceItemId },
include: { priceTable: true },
});
if (priceItem) {
if (priceItem.priceSimple)
basePrice = Number(priceItem.priceSimple);
workName = priceItem.workType;
if (priceItem.priceTable?.unit)
unit = priceItem.priceTable.unit;
}
}
await this.prisma.estimateItem.create({
data: {
estimateId: estimate.id,
orderNumber: i + 1,
sectionType: 'field',
priceItemId: work.priceItemId || null,
workName,
justification: work.justification || null,
basePrice,
quantity: work.volume,
unit,
totalPrice: basePrice * work.volume,
},
});
}
}
return estimate;
}
}
exports.ParserService = ParserService;
/** Значения-заполнители: не перезаписываем ими ранее указанные данные */
ParserService.PLACEHOLDER_VALUES = [
'не указан', 'не указано', 'не указана', 'не указаны',
'—', '', '-', 'н/д', 'н.д.', 'не задан', 'отсутствует', '',
];
//# sourceMappingURL=parser.service.js.map

1
backend/dist/services/parser.service.js.map vendored Executable file

File diff suppressed because one or more lines are too long

55
backend/dist/services/pdf.service.d.ts vendored Executable file
View File

@@ -0,0 +1,55 @@
interface EstimateItem {
orderNumber: number;
workName: string;
justification?: string | null;
basePrice: any;
quantity: any;
unit?: string | null;
coef1?: any;
coef2?: any;
coef3?: any;
totalPrice: any;
sectionType: string;
}
interface EstimateTotal {
orderNumber: number;
label: string;
description?: string | null;
resultValue: any;
}
interface Estimate {
number: string;
direction: {
name: string;
shortName: string;
};
objectName: string;
customer: string;
executor: string;
items: EstimateItem[];
totals: EstimateTotal[];
totalFieldWorks?: any;
totalOfficeWorks?: any;
totalLaboratory?: any;
subtotal?: any;
totalWithoutVat?: any;
vatRate?: any;
vatAmount?: any;
totalWithVat?: any;
}
export declare class PdfService {
/** Resolve path to @fontsource/pt-sans/files (tries require.resolve, then cwd). */
private getFontPaths;
/** Read font buffer from first path that exists. */
private readFontBuffer;
generateEstimatePdf(estimate: Estimate): Promise<Buffer>;
private addHeader;
private addItemsTable;
private addItemRow;
private addTotals;
private addFooter;
private formatNumber;
private formatCurrency;
}
export {};
//# sourceMappingURL=pdf.service.d.ts.map

1
backend/dist/services/pdf.service.d.ts.map vendored Executable file
View File

@@ -0,0 +1 @@
{"version":3,"file":"pdf.service.d.ts","sourceRoot":"","sources":["../../src/services/pdf.service.ts"],"names":[],"mappings":"AAIA,UAAU,YAAY;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE,GAAG,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,UAAU,EAAE,GAAG,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,aAAa;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,GAAG,CAAC;CAClB;AAED,UAAU,QAAQ;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,gBAAgB,CAAC,EAAE,GAAG,CAAC;IACvB,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,eAAe,CAAC,EAAE,GAAG,CAAC;IACtB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,SAAS,CAAC,EAAE,GAAG,CAAC;IAChB,YAAY,CAAC,EAAE,GAAG,CAAC;CACpB;AAOD,qBAAa,UAAU;IACrB,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAapB,oDAAoD;IACpD,OAAO,CAAC,cAAc;IAShB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAmD9D,OAAO,CAAC,SAAS;IA6BjB,OAAO,CAAC,aAAa;IA4FrB,OAAO,CAAC,UAAU;IAkClB,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;CAOvB"}

231
backend/dist/services/pdf.service.js vendored Executable file
View File

@@ -0,0 +1,231 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PdfService = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const pdfkit_1 = __importDefault(require("pdfkit"));
const FONT_FILES = {
regular: 'pt-sans-cyrillic-400-normal.woff',
bold: 'pt-sans-cyrillic-700-normal.woff',
};
class PdfService {
/** Resolve path to @fontsource/pt-sans/files (tries require.resolve, then cwd). */
getFontPaths() {
let dir;
try {
dir = path_1.default.dirname(require.resolve('@fontsource/pt-sans/package.json'));
}
catch {
dir = path_1.default.join(process.cwd(), 'node_modules', '@fontsource', 'pt-sans');
}
return {
regular: path_1.default.join(dir, 'files', FONT_FILES.regular),
bold: path_1.default.join(dir, 'files', FONT_FILES.bold),
};
}
/** Read font buffer from first path that exists. */
readFontBuffer(paths) {
for (const p of paths) {
if (fs_1.default.existsSync(p))
return fs_1.default.readFileSync(p);
}
throw new Error(`Cyrillic font not found. Tried: ${paths.join(', ')}. Run "npm install" in backend and ensure @fontsource/pt-sans is installed.`);
}
async generateEstimatePdf(estimate) {
return new Promise((resolve, reject) => {
try {
const doc = new pdfkit_1.default({
size: 'A4',
margin: 40,
bufferPages: true,
});
const chunks = [];
doc.on('data', (chunk) => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', reject);
// Register Cyrillic fonts (PT Sans) — pass Buffer so path/encoding issues are avoided
const { regular: pathRegular, bold: pathBold } = this.getFontPaths();
const cwd = process.cwd();
const cwdFiles = path_1.default.join(cwd, 'node_modules', '@fontsource', 'pt-sans', 'files');
const parentFiles = path_1.default.join(cwd, '..', 'node_modules', '@fontsource', 'pt-sans', 'files');
const fontRegularBuffer = this.readFontBuffer([
pathRegular,
path_1.default.join(cwdFiles, FONT_FILES.regular),
path_1.default.join(parentFiles, FONT_FILES.regular),
]);
const fontBoldBuffer = this.readFontBuffer([
pathBold,
path_1.default.join(cwdFiles, FONT_FILES.bold),
path_1.default.join(parentFiles, FONT_FILES.bold),
]);
doc.registerFont('PTSans', fontRegularBuffer);
doc.registerFont('PTSansBold', fontBoldBuffer);
// Header
this.addHeader(doc, estimate);
// Items table
this.addItemsTable(doc, estimate);
// Totals section
this.addTotals(doc, estimate);
// Footer
this.addFooter(doc, estimate);
doc.end();
}
catch (error) {
reject(error);
}
});
}
addHeader(doc, estimate) {
doc.fontSize(14).font('PTSansBold');
doc.text(`Исполнительная смета №${estimate.number}`, { align: 'center' });
doc.moveDown(0.5);
doc.fontSize(12).font('PTSans');
doc.text(`${estimate.direction.name} на объекте:`, { align: 'center' });
doc.moveDown(0.5);
doc.fontSize(11);
doc.text(`«${estimate.objectName}»`, { align: 'center' });
doc.moveDown(1);
// Customer and Executor
const leftX = 40;
const rightX = 300;
doc.fontSize(10);
doc.text('Наименование организации Заказчика:', leftX, doc.y);
doc.text(`«${estimate.customer}»`, rightX, doc.y - 10);
doc.moveDown(0.5);
doc.text('Наименование организации Исполнителя:', leftX, doc.y);
doc.text(`«${estimate.executor}»`, rightX, doc.y - 10);
doc.moveDown(1);
}
addItemsTable(doc, estimate) {
const tableTop = doc.y;
const tableLeft = 40;
const colWidths = [30, 200, 100, 50, 50, 85]; // №, Наименование, Обоснование, Цена, Объем, Стоимость
doc.fontSize(8).font('PTSansBold');
// Table header
let x = tableLeft;
const headers = ['№', 'Наименование работ', 'Обоснование', 'Цена', 'Объем', 'Стоимость'];
headers.forEach((header, i) => {
doc.text(header, x + 2, tableTop, { width: colWidths[i] - 4, align: 'center' });
x += colWidths[i];
});
doc.moveTo(tableLeft, tableTop - 5)
.lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), tableTop - 5)
.stroke();
let y = tableTop + 15;
doc.moveTo(tableLeft, y)
.lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), y)
.stroke();
y += 5;
doc.font('PTSans').fontSize(7);
// Group items by section
const sections = {
field: [],
office: [],
laboratory: [],
other: [],
};
estimate.items.forEach(item => {
const section = sections[item.sectionType] || sections.other;
section.push(item);
});
// Field works
if (sections.field.length > 0) {
doc.font('PTSansBold').fontSize(8);
doc.text('Полевые работы', tableLeft + 2, y);
y += 12;
doc.font('PTSans').fontSize(7);
sections.field.forEach(item => {
y = this.addItemRow(doc, item, y, tableLeft, colWidths);
});
}
// Office works
if (sections.office.length > 0) {
doc.font('PTSansBold').fontSize(8);
doc.text('Камеральные работы', tableLeft + 2, y);
y += 12;
doc.font('PTSans').fontSize(7);
sections.office.forEach(item => {
y = this.addItemRow(doc, item, y, tableLeft, colWidths);
});
}
// Laboratory works
if (sections.laboratory.length > 0) {
doc.font('PTSansBold').fontSize(8);
doc.text('Лабораторные работы', tableLeft + 2, y);
y += 12;
doc.font('PTSans').fontSize(7);
sections.laboratory.forEach(item => {
y = this.addItemRow(doc, item, y, tableLeft, colWidths);
});
}
// Other items
if (sections.other.length > 0) {
sections.other.forEach(item => {
y = this.addItemRow(doc, item, y, tableLeft, colWidths);
});
}
// Subtotal line
doc.moveTo(tableLeft, y)
.lineTo(tableLeft + colWidths.reduce((a, b) => a + b, 0), y)
.stroke();
doc.y = y + 10;
}
addItemRow(doc, item, y, tableLeft, colWidths) {
const rowHeight = 20;
let x = tableLeft;
// Check for page break
if (y > 750) {
doc.addPage();
y = 40;
}
// Row data
const cells = [
String(item.orderNumber),
item.workName.substring(0, 60),
(item.justification || '').substring(0, 30),
this.formatNumber(Number(item.basePrice)),
this.formatNumber(Number(item.quantity)),
this.formatNumber(Number(item.totalPrice)),
];
cells.forEach((cell, i) => {
doc.text(cell, x + 2, y, { width: colWidths[i] - 4, align: i > 2 ? 'right' : 'left' });
x += colWidths[i];
});
return y + rowHeight;
}
addTotals(doc, estimate) {
const leftX = 300;
const rightX = 480;
doc.moveDown(1);
doc.font('PTSans').fontSize(9);
estimate.totals.forEach(total => {
doc.text(total.label, leftX, doc.y, { continued: true });
if (total.description) {
doc.text(` (${total.description})`, { continued: true });
}
doc.text('');
doc.text(this.formatCurrency(Number(total.resultValue)), rightX, doc.y - 10, { align: 'right' });
doc.moveDown(0.3);
});
}
addFooter(doc, estimate) {
doc.moveDown(2);
doc.font('PTSans').fontSize(9);
doc.text('Выполнил: _____________________', 40);
}
formatNumber(num) {
return num.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
formatCurrency(num) {
return num.toLocaleString('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 2,
});
}
}
exports.PdfService = PdfService;
//# sourceMappingURL=pdf.service.js.map

1
backend/dist/services/pdf.service.js.map vendored Executable file

File diff suppressed because one or more lines are too long

5
backend/fonts/README.md Executable file
View File

@@ -0,0 +1,5 @@
# Шрифты для PDF
Для генерации PDF с кириллицей используются шрифты PT Sans из npm-пакета `@fontsource/pt-sans` (файлы в `node_modules/@fontsource/pt-sans/files/`).
При необходимости можно положить сюда свои TTF/WOFF (например, PTSans-Regular.ttf и PTSans-Bold.ttf) — тогда в `pdf.service.ts` нужно указать путь к этой папке через `path.join(process.cwd(), 'fonts', '...')`.

16
backend/node_modules/.bin/acorn generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
else
exec node "$basedir/../acorn/bin/acorn" "$@"
fi

17
backend/node_modules/.bin/acorn.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\acorn\bin\acorn" %*

28
backend/node_modules/.bin/acorn.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
} else {
& "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../acorn/bin/acorn" $args
} else {
& "node$exe" "$basedir/../acorn/bin/acorn" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/color-support generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../color-support/bin.js" "$@"
else
exec node "$basedir/../color-support/bin.js" "$@"
fi

17
backend/node_modules/.bin/color-support.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\color-support\bin.js" %*

28
backend/node_modules/.bin/color-support.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../color-support/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../color-support/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../color-support/bin.js" $args
} else {
& "node$exe" "$basedir/../color-support/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/crc32 generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../crc-32/bin/crc32.njs" "$@"
else
exec node "$basedir/../crc-32/bin/crc32.njs" "$@"
fi

17
backend/node_modules/.bin/crc32.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\crc-32\bin\crc32.njs" %*

28
backend/node_modules/.bin/crc32.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
} else {
& "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
} else {
& "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/mime generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
else
exec node "$basedir/../mime/cli.js" "$@"
fi

17
backend/node_modules/.bin/mime.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*

28
backend/node_modules/.bin/mime.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mime/cli.js" $args
} else {
& "node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/mkdirp generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
else
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
fi

17
backend/node_modules/.bin/mkdirp.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*

28
backend/node_modules/.bin/mkdirp.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
} else {
& "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/node-pre-gyp generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" "$@"
else
exec node "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" "$@"
fi

17
backend/node_modules/.bin/node-pre-gyp.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@mapbox\node-pre-gyp\bin\node-pre-gyp" %*

28
backend/node_modules/.bin/node-pre-gyp.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" $args
} else {
& "$basedir/node$exe" "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" $args
} else {
& "node$exe" "$basedir/../@mapbox/node-pre-gyp/bin/node-pre-gyp" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/nopt generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nopt/bin/nopt.js" "$@"
else
exec node "$basedir/../nopt/bin/nopt.js" "$@"
fi

17
backend/node_modules/.bin/nopt.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nopt\bin\nopt.js" %*

28
backend/node_modules/.bin/nopt.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nopt/bin/nopt.js" $args
} else {
& "$basedir/node$exe" "$basedir/../nopt/bin/nopt.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nopt/bin/nopt.js" $args
} else {
& "node$exe" "$basedir/../nopt/bin/nopt.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/prisma generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../prisma/build/index.js" "$@"
else
exec node "$basedir/../prisma/build/index.js" "$@"
fi

17
backend/node_modules/.bin/prisma.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prisma\build\index.js" %*

28
backend/node_modules/.bin/prisma.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/resolve generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../resolve/bin/resolve" "$@"
else
exec node "$basedir/../resolve/bin/resolve" "$@"
fi

17
backend/node_modules/.bin/resolve.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\resolve\bin\resolve" %*

28
backend/node_modules/.bin/resolve.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
} else {
& "$basedir/node$exe" "$basedir/../resolve/bin/resolve" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../resolve/bin/resolve" $args
} else {
& "node$exe" "$basedir/../resolve/bin/resolve" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/rimraf generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
else
exec node "$basedir/../rimraf/bin.js" "$@"
fi

17
backend/node_modules/.bin/rimraf.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rimraf\bin.js" %*

28
backend/node_modules/.bin/rimraf.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rimraf/bin.js" $args
} else {
& "node$exe" "$basedir/../rimraf/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/semver generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
else
exec node "$basedir/../semver/bin/semver.js" "$@"
fi

17
backend/node_modules/.bin/semver.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*

28
backend/node_modules/.bin/semver.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/tree-kill generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tree-kill/cli.js" "$@"
else
exec node "$basedir/../tree-kill/cli.js" "$@"
fi

17
backend/node_modules/.bin/tree-kill.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tree-kill\cli.js" %*

28
backend/node_modules/.bin/tree-kill.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../tree-kill/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../tree-kill/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../tree-kill/cli.js" $args
} else {
& "node$exe" "$basedir/../tree-kill/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/ts-node generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin.js" "$@"
fi

16
backend/node_modules/.bin/ts-node-cwd generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-cwd.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-cwd.js" "$@"
fi

17
backend/node_modules/.bin/ts-node-cwd.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\ts-node\dist\bin-cwd.js" %*

28
backend/node_modules/.bin/ts-node-cwd.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../ts-node/dist/bin-cwd.js" $args
} else {
& "$basedir/node$exe" "$basedir/../ts-node/dist/bin-cwd.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../ts-node/dist/bin-cwd.js" $args
} else {
& "node$exe" "$basedir/../ts-node/dist/bin-cwd.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/ts-node-dev generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node-dev/lib/bin.js" "$@"
else
exec node "$basedir/../ts-node-dev/lib/bin.js" "$@"
fi

17
backend/node_modules/.bin/ts-node-dev.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\ts-node-dev\lib\bin.js" %*

28
backend/node_modules/.bin/ts-node-dev.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../ts-node-dev/lib/bin.js" $args
} else {
& "$basedir/node$exe" "$basedir/../ts-node-dev/lib/bin.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../ts-node-dev/lib/bin.js" $args
} else {
& "node$exe" "$basedir/../ts-node-dev/lib/bin.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/ts-node-esm generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-esm.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-esm.js" "$@"
fi

17
backend/node_modules/.bin/ts-node-esm.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\ts-node\dist\bin-esm.js" %*

28
backend/node_modules/.bin/ts-node-esm.ps1 generated vendored Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../ts-node/dist/bin-esm.js" $args
} else {
& "$basedir/node$exe" "$basedir/../ts-node/dist/bin-esm.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../ts-node/dist/bin-esm.js" $args
} else {
& "node$exe" "$basedir/../ts-node/dist/bin-esm.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
backend/node_modules/.bin/ts-node-script generated vendored Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-script.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-script.js" "$@"
fi

17
backend/node_modules/.bin/ts-node-script.cmd generated vendored Executable file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\ts-node\dist\bin-script.js" %*

Some files were not shown because too many files have changed in this diff Show More