Files
mkd/backend/routes/buildings.js
2026-02-04 00:17:04 +05:00

451 lines
18 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const express = require('express');
const BUILDINGS_CACHE_TTL_MS = 30 * 1000; // 30 секунд
function createResponseCache() {
const store = new Map();
return {
get(key) {
const entry = store.get(key);
if (!entry || Date.now() > entry.expiresAt) {
if (entry) store.delete(key);
return null;
}
return entry.data;
},
set(key, data, ttlMs = BUILDINGS_CACHE_TTL_MS) {
store.set(key, { data, expiresAt: Date.now() + ttlMs });
},
invalidatePrefix(prefix) {
for (const key of store.keys()) {
if (key.startsWith(prefix)) store.delete(key);
}
}
};
}
/**
* Buildings API router.
* @param {{ query: (text: string, params?: any[]) => Promise<any[]> }} deps
* @returns {express.Router}
*/
function createBuildingsRouter(deps) {
const { query } = deps;
const router = express.Router();
const cache = createResponseCache();
function invalidateBuildingsCache() {
cache.invalidatePrefix('buildings:');
}
// GET /buildings -> список домов (пагинация, опционально ?light=1). При scope=own_district — дома назначенных участков (employee_districts + fallback assigned_district_id).
router.get('/buildings', async (req, res) => {
try {
const limit = Math.min(Math.max(1, parseInt(req.query.limit, 10) || 100), 500);
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
const light = req.query.light === '1' || req.query.light === 'true';
let districtIds = null; // null = все участки, [] = нет доступа, [id, ...] = только эти участки
if (req.user && req.user.userId) {
const userScopeRows = await query(
`SELECT pu.scope, e.id AS employee_id, e.assigned_district_id
FROM portal_users pu
JOIN employees e ON e.id = pu.employee_id
WHERE pu.id = $1`,
[req.user.userId]
);
const row = userScopeRows[0];
if (row && row.scope === 'own_district') {
let ids = [];
try {
const edRows = await query(
'SELECT district_id FROM employee_districts WHERE employee_id = $1 ORDER BY district_id',
[row.employee_id]
);
ids = (edRows || []).map((r) => r.district_id).filter(Boolean);
} catch (_) {}
if (ids.length === 0 && row.assigned_district_id) {
ids = [row.assigned_district_id];
}
districtIds = ids;
}
}
const cacheKey = districtIds === null
? `buildings:list:${limit}:${offset}:${light}`
: `buildings:list:${limit}:${offset}:${light}:districts:${[...districtIds].sort().join(',')}`;
const cached = districtIds === null ? cache.get(cacheKey) : null;
if (cached !== null) {
return res.json(cached);
}
let rows;
if (districtIds !== null && districtIds.length === 0) {
rows = [];
} else if (districtIds !== null && districtIds.length > 0) {
rows = await query(
`SELECT id, data FROM buildings WHERE (data->>'districtId') = ANY($1::text[]) ORDER BY id LIMIT $2 OFFSET $3`,
[districtIds, limit, offset]
);
} else {
rows = await query(
'SELECT id, data FROM buildings ORDER BY id LIMIT $1 OFFSET $2',
[limit, offset]
);
}
let data;
if (light) {
data = rows.map((r) => {
const d = r.data || {};
return {
id: d.id || r.id,
districtId: d.districtId ?? null,
passport: d.passport ? { address: d.passport.address || '' } : { address: '' }
};
});
} else {
data = rows.map((r) => r.data);
const ids = rows.map((r) => (r.data && r.data.id) || r.id).filter(Boolean);
if (ids.length > 0) {
try {
const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');
const accountRows = await query(
`SELECT building_id, data FROM building_personal_accounts WHERE building_id IN (${placeholders})`,
ids
);
const accountsByBuilding = {};
for (const ar of accountRows) {
if (!accountsByBuilding[ar.building_id]) accountsByBuilding[ar.building_id] = [];
accountsByBuilding[ar.building_id].push(ar.data);
}
for (const b of data) {
const bid = b.id;
b.accounts = Array.isArray(accountsByBuilding[bid]) ? accountsByBuilding[bid] : (Array.isArray(b.accounts) ? b.accounts : []);
}
} catch (e) {
if (!e.message || !e.message.includes('building_personal_accounts')) {
console.warn('Error loading accounts for list:', e.message);
}
}
}
}
if (districtIds === null) cache.set(cacheKey, data);
res.json(data);
} catch (err) {
console.error('Error fetching buildings:', err);
res.status(500).json({ error: 'Failed to fetch buildings' });
}
});
// POST /buildings -> создание нового дома
router.post('/buildings', async (req, res) => {
const building = req.body;
if (!building || !building.id || !building.passport || !building.passport.address) {
return res
.status(400)
.json({ error: 'Ожидается объект Building с полями id и passport.address' });
}
try {
await query('INSERT INTO buildings (id, data) VALUES ($1, $2)', [
building.id,
building,
]);
try {
const existingSurvey = await query(
`SELECT id FROM nps_surveys WHERE building_id = $1`,
[building.id]
);
if (existingSurvey.length === 0) {
const accessKey = `nps-${building.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
await query(
`INSERT INTO nps_surveys
(building_id, title, description, status, access_key, created_by)
VALUES ($1, $2, $3, $4, $5, 'system')`,
[
building.id,
'Опрос удовлетворенности жителей',
'Помогите нам стать лучше! Поделитесь своим мнением о качестве обслуживания.',
'draft',
accessKey
]
);
console.log(`[Auto NPS] Создан опрос для нового дома ${building.id}`);
}
} catch (npsErr) {
console.error(`[Auto NPS] Ошибка создания опроса для дома ${building.id}:`, npsErr);
}
try {
const now = new Date();
const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
const months = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
const monthName = months[now.getMonth()];
const reportMonth = `${monthName} ${now.getFullYear()}`;
const existingReport = await query(
`SELECT id FROM resident_reports
WHERE building_id = $1 AND month = $2`,
[building.id, reportMonth]
);
if (existingReport.length === 0) {
const initialContent = {
applications: { total: 156, completed: 153, quality: 98 },
finances: { collected: 2400000, expenses: 1890000, balance: 560000 },
nps: { score: 72, totalResponses: 45 }
};
const newReport = await query(
`INSERT INTO resident_reports
(building_id, month, period_start, period_end, status, content, published_at)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, NOW())
RETURNING id`,
[
building.id,
reportMonth,
currentMonthStart.toISOString().split('T')[0],
currentMonthEnd.toISOString().split('T')[0],
'published',
JSON.stringify(initialContent)
]
);
console.log(`[Auto Report] Создан опубликованный отчет для нового дома ${building.id} (ID: ${newReport[0].id})`);
}
} catch (reportErr) {
console.error(`[Auto Report] Ошибка создания отчета для дома ${building.id}:`, reportErr);
}
invalidateBuildingsCache();
res.status(201).json(building);
} catch (err) {
console.error('Error creating building:', err);
res.status(500).json({ error: 'Failed to create building' });
}
});
// GET /buildings/:id — здание + лицевые счета (из building_personal_accounts или из data)
router.get('/buildings/:id', async (req, res) => {
try {
const { id } = req.params;
const cacheKey = `buildings:id:${id}`;
const cached = cache.get(cacheKey);
if (cached !== null) {
return res.json(cached);
}
const rows = await query('SELECT data FROM buildings WHERE id = $1', [id]);
if (!rows.length) {
return res.status(404).json({ error: 'Дом не найден' });
}
const data = rows[0].data;
try {
const accountRows = await query(
'SELECT id, data FROM building_personal_accounts WHERE building_id = $1',
[id]
);
if (accountRows.length > 0) {
data.accounts = accountRows.map((r) => r.data);
} else if (!Array.isArray(data.accounts)) {
data.accounts = [];
}
} catch (e) {
if (!Array.isArray(data.accounts)) data.accounts = [];
}
cache.set(cacheKey, data);
res.json(data);
} catch (err) {
console.error('Error fetching building:', err);
res.status(500).json({ error: err.message || 'Failed to fetch building' });
}
});
// PUT /buildings/:id — если есть таблица building_personal_accounts: не пишем accounts в data; иначе пишем всё (fallback)
router.put('/buildings/:id', async (req, res) => {
const { id } = req.params;
const building = req.body;
if (!building || !building.passport || !building.passport.address) {
return res
.status(400)
.json({ error: 'Ожидается объект Building с полями passport.address' });
}
try {
let useAccountsTable = false;
try {
await query('SELECT 1 FROM building_personal_accounts LIMIT 1');
useAccountsTable = true;
} catch (_) {}
const toSave = { ...building };
if (useAccountsTable) delete toSave.accounts;
await query('UPDATE buildings SET data = $2 WHERE id = $1', [id, toSave]);
invalidateBuildingsCache();
const dataForResponse = { ...toSave };
if (useAccountsTable) {
try {
const accountRows = await query(
'SELECT id, data FROM building_personal_accounts WHERE building_id = $1',
[id]
);
dataForResponse.accounts = accountRows.length > 0 ? accountRows.map((r) => r.data) : [];
} catch (e) {
dataForResponse.accounts = Array.isArray(building.accounts) ? building.accounts : [];
}
} else {
dataForResponse.accounts = Array.isArray(building.accounts) ? building.accounts : [];
}
res.json(dataForResponse);
} catch (err) {
console.error('Error updating building:', err);
res.status(500).json({ error: 'Failed to update building' });
}
});
// DELETE /buildings/:id
router.delete('/buildings/:id', async (req, res) => {
const { id } = req.params;
try {
const buildingRows = await query('SELECT id FROM buildings WHERE id = $1', [id]);
if (buildingRows.length === 0) {
return res.status(404).json({ error: 'Дом не найден' });
}
await query('DELETE FROM buildings WHERE id = $1', [id]);
invalidateBuildingsCache();
res.json({ success: true, message: 'Дом успешно удален' });
} catch (err) {
console.error('Error deleting building:', err);
res.status(500).json({ error: 'Ошибка при удалении дома' });
}
});
// POST /buildings/:id/accounts — в building_personal_accounts или в buildings.data (fallback)
router.post('/buildings/:id/accounts', async (req, res) => {
const { id: buildingId } = req.params;
const newAccount = req.body;
if (!newAccount || !newAccount.apartmentNumber) {
return res.status(400).json({ error: 'Ожидается объект PersonalAccount с полем apartmentNumber' });
}
try {
const buildingRows = await query('SELECT data FROM buildings WHERE id = $1', [buildingId]);
if (buildingRows.length === 0) {
return res.status(404).json({ error: 'Building not found' });
}
let useAccountsTable = false;
try {
await query('SELECT 1 FROM building_personal_accounts LIMIT 1');
useAccountsTable = true;
} catch (_) {}
const accounts = Array.isArray(buildingRows[0].data?.accounts) ? buildingRows[0].data.accounts : [];
if (!newAccount.id) newAccount.id = `${buildingId}-acc-${Date.now()}`;
if (!newAccount.accountNumber) newAccount.accountNumber = `${buildingId.replace('b-', '')}00${accounts.length + 1}`;
if (useAccountsTable) {
await query(
'INSERT INTO building_personal_accounts (id, building_id, data) VALUES ($1, $2, $3)',
[newAccount.id, buildingId, newAccount]
);
} else {
const building = buildingRows[0].data;
building.accounts = [...accounts, newAccount];
building.isDirty = true;
await query('UPDATE buildings SET data = $2 WHERE id = $1', [buildingId, building]);
}
invalidateBuildingsCache();
res.status(201).json(newAccount);
} catch (err) {
console.error('Error creating account:', err);
res.status(500).json({ error: 'Failed to create account' });
}
});
// PUT /buildings/:id/accounts/:accountId — обновление в building_personal_accounts или в data (fallback)
router.put('/buildings/:id/accounts/:accountId', async (req, res) => {
const { id: buildingId, accountId } = req.params;
const updatedAccount = req.body;
if (!updatedAccount || !updatedAccount.id) {
return res.status(400).json({ error: 'Ожидается объект PersonalAccount с полем id' });
}
try {
const buildingRows = await query('SELECT data FROM buildings WHERE id = $1', [buildingId]);
if (buildingRows.length === 0) {
return res.status(404).json({ error: 'Building not found' });
}
let useAccountsTable = false;
try {
await query('SELECT 1 FROM building_personal_accounts LIMIT 1');
useAccountsTable = true;
} catch (_) {}
if (useAccountsTable) {
const updateResult = await query(
'UPDATE building_personal_accounts SET data = $3 WHERE id = $1 AND building_id = $2 RETURNING id',
[accountId, buildingId, updatedAccount]
);
if (updateResult.length === 0) {
return res.status(404).json({ error: 'Account not found' });
}
} else {
const building = buildingRows[0].data;
const accounts = Array.isArray(building.accounts) ? building.accounts : [];
const idx = accounts.findIndex((a) => a.id === accountId);
if (idx === -1) return res.status(404).json({ error: 'Account not found' });
building.accounts = accounts.map((a) => (a.id === accountId ? updatedAccount : a));
building.isDirty = true;
await query('UPDATE buildings SET data = $2 WHERE id = $1', [buildingId, building]);
}
invalidateBuildingsCache();
res.json(updatedAccount);
} catch (err) {
console.error('Error updating account:', err);
res.status(500).json({ error: 'Failed to update account' });
}
});
// DELETE /buildings/:id/accounts/:accountId — из building_personal_accounts или из data (fallback)
router.delete('/buildings/:id/accounts/:accountId', async (req, res) => {
const { id: buildingId, accountId } = req.params;
try {
const buildingRows = await query('SELECT data FROM buildings WHERE id = $1', [buildingId]);
if (buildingRows.length === 0) {
return res.status(404).json({ error: 'Building not found' });
}
let useAccountsTable = false;
try {
await query('SELECT 1 FROM building_personal_accounts LIMIT 1');
useAccountsTable = true;
} catch (_) {}
if (useAccountsTable) {
const deleteResult = await query(
'DELETE FROM building_personal_accounts WHERE id = $1 AND building_id = $2 RETURNING id',
[accountId, buildingId]
);
if (deleteResult.length === 0) {
return res.status(404).json({ error: 'Account not found' });
}
} else {
const building = buildingRows[0].data;
const accounts = Array.isArray(building.accounts) ? building.accounts : [];
const filtered = accounts.filter((a) => a.id !== accountId);
if (filtered.length === accounts.length) {
return res.status(404).json({ error: 'Account not found' });
}
building.accounts = filtered;
building.isDirty = true;
await query('UPDATE buildings SET data = $2 WHERE id = $1', [buildingId, building]);
}
invalidateBuildingsCache();
res.json({ success: true, message: 'Account deleted' });
} catch (err) {
console.error('Error deleting account:', err);
res.status(500).json({ error: 'Failed to delete account' });
}
});
return router;
}
module.exports = createBuildingsRouter;