Первый коммит после распаковки архива

This commit is contained in:
2025-08-14 20:14:42 +03:00
commit 5d4e9ba201
354 changed files with 40492 additions and 0 deletions

384
server/organizations.js Normal file
View File

@@ -0,0 +1,384 @@
const express = require('express');
const db = require('../db');
const jwt = require('jsonwebtoken');
module.exports = function(io, onlineUsers) {
const router = express.Router();
function authenticate(req, res, next) {
const auth = req.headers.authorization?.split(' ');
try {
if (!auth || auth[0] !== 'Bearer') return res.status(401).send('No token');
const payload = jwt.verify(auth[1], process.env.JWT_SECRET);
req.user = payload;
next();
} catch {
res.status(401).send('Invalid token');
}
}
async function initTables() {
await db.query(`CREATE TABLE IF NOT EXISTS organizations (
id SERIAL PRIMARY KEY,
object_id INTEGER,
name TEXT NOT NULL,
type TEXT NOT NULL,
city TEXT,
owner TEXT,
budget NUMERIC DEFAULT 0,
rating NUMERIC DEFAULT 0
)`);
await db.query(`ALTER TABLE organizations ADD COLUMN IF NOT EXISTS object_id INTEGER`);
// NEW: владелец числовой
await db.query(`ALTER TABLE organizations ADD COLUMN IF NOT EXISTS owner_id INTEGER`);
await db.query(`CREATE TABLE IF NOT EXISTS organization_settings (
organization_id INTEGER PRIMARY KEY REFERENCES organizations(id) ON DELETE CASCADE,
menu JSONB,
work_hours TEXT
)`);
await db.query(`CREATE TABLE IF NOT EXISTS organization_orders (
id SERIAL PRIMARY KEY,
organization_id INTEGER REFERENCES organizations(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
item TEXT NOT NULL,
price NUMERIC NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`);
await db.query(`CREATE TABLE IF NOT EXISTS organization_inventory (
id SERIAL PRIMARY KEY,
organization_id INTEGER REFERENCES organizations(id) ON DELETE CASCADE,
item TEXT NOT NULL,
quantity INTEGER DEFAULT 0,
price NUMERIC DEFAULT 0
)`);
await db.query(`CREATE TABLE IF NOT EXISTS organization_employees (
organization_id INTEGER REFERENCES organizations(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
wage NUMERIC DEFAULT 0,
PRIMARY KEY(organization_id, user_id)
)`);
await db.query(`CREATE TABLE IF NOT EXISTS hotel_rooms (
id SERIAL PRIMARY KEY,
organization_id INTEGER REFERENCES organizations(id) ON DELETE CASCADE,
room_number TEXT,
class TEXT,
price NUMERIC,
is_available BOOLEAN DEFAULT true,
occupant INTEGER REFERENCES users(id),
lease_end TIMESTAMPTZ
)`);
}
initTables().catch(err => console.error('Failed to init organization tables', err));
router.post('/', async (req, res) => {
const { name, type, city, owner, ownerId, objectId, budget = 0, rating = 0 } = req.body;
try {
const { rows } = await db.query(
`INSERT INTO organizations(name, type, city, owner, owner_id, object_id, budget, rating)
VALUES($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
[name, type, city, owner ?? null, ownerId ?? null, objectId, budget, rating]
);
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'db error' });
}
});
router.get('/:id', async (req, res) => {
const { id } = req.params;
const { rows } = await db.query('SELECT * FROM organizations WHERE id=$1', [id]);
if (!rows.length) return res.status(404).json({ error: 'not found' });
res.json(rows[0]);
});
router.get('/object/:objectId', async (req, res) => {
const { objectId } = req.params;
const { rows } = await db.query('SELECT * FROM organizations WHERE object_id=$1', [objectId]);
if (!rows.length) return res.status(404).json({ error: 'not found' });
res.json(rows[0]);
});
// >>> НОРМАЛИЗУЕМ МЕНЮ В МАССИВ
router.get('/:id/settings', async (req, res) => {
const { id } = req.params;
const { rows } = await db.query(
'SELECT menu, work_hours FROM organization_settings WHERE organization_id=$1',
[id]
);
const raw = rows[0] || {};
const menu = raw.menu || {};
const menuArray = Array.isArray(menu)
? menu
: Object.entries(menu).map(([key, val]) => ({ key, ...(val || {}) }));
res.json({ menu: menuArray, work_hours: raw.work_hours || '' });
});
router.put('/:id/settings', async (req, res) => {
const { id } = req.params;
let { menu, workHours } = req.body;
// принимаем и массив, и объект — храним как есть
try {
const { rows } = await db.query(
`INSERT INTO organization_settings(organization_id, menu, work_hours)
VALUES($1, $2, $3)
ON CONFLICT (organization_id)
DO UPDATE SET menu = EXCLUDED.menu, work_hours = EXCLUDED.work_hours
RETURNING menu, work_hours`,
[id, menu, workHours]
);
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/purchase', authenticate, async (req, res) => {
const { id } = req.params;
const { itemKey } = req.body;
try {
const menuRes = await db.query('SELECT menu FROM organization_settings WHERE organization_id=$1', [id]);
const menu = menuRes.rows[0]?.menu || {};
const entry = Array.isArray(menu)
? (menu.find(it => it.key === itemKey) || null)
: menu[itemKey];
if (!entry) return res.status(400).json({ error: 'invalid item' });
let itemDef;
if (entry.itemId) {
const r = await db.query('SELECT * FROM items WHERE id=$1', [entry.itemId]);
itemDef = r.rows[0];
} else {
const r = await db.query('SELECT * FROM items WHERE key=$1', [itemKey]);
itemDef = r.rows[0];
}
if (!itemDef) return res.status(400).json({ error: 'item not found' });
const price = parseFloat(entry.price);
const { rows } = await db.query('SELECT balance, satiety, thirst FROM users WHERE id=$1', [req.user.id]);
let balance = parseFloat(rows[0].balance);
let satiety = parseFloat(rows[0].satiety ?? 100);
let thirst = parseFloat(rows[0].thirst ?? 100);
if (balance < price) {
const sock = onlineUsers[req.user.id];
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вам недостаточно средств для покупки ${itemDef.name}` });
return res.status(400).json({ error: 'insufficient funds' });
}
await db.query('BEGIN');
await db.query(
'UPDATE users SET balance = balance - $1, satiety = LEAST(100, COALESCE(satiety,100)+$2), thirst = LEAST(100, COALESCE(thirst,100)+$3) WHERE id=$4',
[price, itemDef.hunger_gain, itemDef.thirst_gain, req.user.id]
);
await db.query(
`INSERT INTO organization_orders(organization_id, user_id, item, price)
VALUES($1,$2,$3,$4)`,
[id, req.user.id, itemDef.name, price]
);
await db.query(
`INSERT INTO inventory(user_id, item_id, name, quantity, stackable, weight)
VALUES($1,$2,$3,1,$4,$5)
ON CONFLICT (user_id, item_id) DO UPDATE SET quantity = inventory.quantity + 1`,
[req.user.id, itemDef.id, itemDef.name, itemDef.stackable, itemDef.weight]
);
await db.query('COMMIT');
balance -= price;
satiety = Math.min(100, satiety + parseFloat(itemDef.hunger_gain));
thirst = Math.min(100, thirst + parseFloat(itemDef.thirst_gain));
const sock = onlineUsers[req.user.id];
if (sock) io.to(sock).emit('chatMessage', { playerId: 0, name: 'Система', message: `Вы купили ${itemDef.name}` });
res.json({ success: true, balance, satiety, thirst });
} catch (err) {
await db.query('ROLLBACK');
console.error(err);
res.status(500).json({ error: 'db error' });
}
});
// ----- управление бизнесом -----
// Ставим owner_id (и дублируем в owner для совместимости)
router.put('/:id/owner', authenticate, async (req, res) => {
const { id } = req.params;
const { ownerId } = req.body; // число
try {
await db.query('UPDATE organizations SET owner_id=$1, owner=$1::text WHERE id=$2', [ownerId, id]);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.delete('/:id', authenticate, async (req, res) => {
const { id } = req.params;
try {
await db.query('DELETE FROM organizations WHERE id=$1', [id]);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/inventory', authenticate, async (req, res) => {
const { id } = req.params;
const { item, quantity = 0, cost = 0, price = 0 } = req.body;
try {
await db.query('BEGIN');
await db.query(
`INSERT INTO organization_inventory(organization_id, item, quantity, price)
VALUES($1,$2,$3,$4)
ON CONFLICT (organization_id, item)
DO UPDATE SET quantity = organization_inventory.quantity + EXCLUDED.quantity, price = EXCLUDED.price`,
[id, item, quantity, price]
);
await db.query('UPDATE organizations SET budget = budget - $1 WHERE id=$2', [cost, id]);
await db.query('COMMIT');
res.json({ success: true });
} catch (e) {
await db.query('ROLLBACK');
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.get('/:id/inventory', async (req, res) => {
const { id } = req.params;
try {
const { rows } = await db.query('SELECT * FROM organization_inventory WHERE organization_id=$1', [id]);
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/pay-wages', authenticate, async (req, res) => {
const { id } = req.params;
try {
const { rows } = await db.query('SELECT user_id, wage FROM organization_employees WHERE organization_id=$1', [id]);
let total = rows.reduce((s, r) => s + parseFloat(r.wage), 0);
const org = await db.query('SELECT budget FROM organizations WHERE id=$1', [id]);
if (parseFloat(org.rows[0].budget) < total) return res.status(400).json({ error: 'insufficient funds' });
await db.query('BEGIN');
await db.query('UPDATE organizations SET budget = budget - $1 WHERE id=$2', [total, id]);
for (const r of rows) {
await db.query('UPDATE users SET balance = balance + $1 WHERE id=$2', [r.wage, r.user_id]);
}
await db.query('COMMIT');
res.json({ success: true });
} catch (e) {
await db.query('ROLLBACK');
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/pay-taxes', authenticate, async (req, res) => {
const { id } = req.params;
const { amount } = req.body;
try {
await db.query('UPDATE organizations SET budget = budget - $1 WHERE id=$2', [amount, id]);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/pay-utilities', authenticate, async (req, res) => {
const { id } = req.params;
const { amount } = req.body;
try {
await db.query('UPDATE organizations SET budget = budget - $1 WHERE id=$2', [amount, id]);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
// ----- управление отелями -----
router.post('/:id/rooms', authenticate, async (req, res) => {
const { id } = req.params;
const { roomNumber, class: roomClass, price } = req.body;
try {
const { rows } = await db.query(
`INSERT INTO hotel_rooms(organization_id, room_number, class, price)
VALUES($1,$2,$3,$4) RETURNING *`,
[id, roomNumber, roomClass, price]
);
res.json(rows[0]);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.put('/:id/rooms/:roomId', authenticate, async (req, res) => {
const { id, roomId } = req.params;
const { class: roomClass, price } = req.body;
try {
await db.query(
`UPDATE hotel_rooms SET class=COALESCE($1,class), price=COALESCE($2,price) WHERE id=$3 AND organization_id=$4`,
[roomClass, price, roomId, id]
);
res.json({ success: true });
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.get('/:id/rooms', async (req, res) => {
const { id } = req.params;
try {
const { rows } = await db.query('SELECT * FROM hotel_rooms WHERE organization_id=$1', [id]);
res.json(rows);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
router.post('/:id/rooms/:roomId/rent', authenticate, async (req, res) => {
const { id, roomId } = req.params;
const { days = 1 } = req.body;
try {
const { rows } = await db.query('SELECT price, is_available FROM hotel_rooms WHERE id=$1 AND organization_id=$2', [roomId, id]);
if (!rows.length || !rows[0].is_available) return res.status(400).json({ error: 'room unavailable' });
const price = parseFloat(rows[0].price) * days;
const user = await db.query('SELECT balance FROM users WHERE id=$1', [req.user.id]);
if (parseFloat(user.rows[0].balance) < price) return res.status(400).json({ error: 'insufficient funds' });
await db.query('BEGIN');
await db.query('UPDATE users SET balance = balance - $1 WHERE id=$2', [price, req.user.id]);
await db.query('UPDATE organizations SET budget = budget + $1 WHERE id=$2', [price, id]);
await db.query(
'UPDATE hotel_rooms SET is_available=false, occupant=$1, lease_end = NOW() + ($2 || " days")::interval WHERE id=$3',
[req.user.id, days, roomId]
);
await db.query('COMMIT');
res.json({ success: true });
} catch (e) {
await db.query('ROLLBACK');
console.error(e);
res.status(500).json({ error: 'db error' });
}
});
return router;
};