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

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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"}