liftman/server.js
2026-05-22 23:05:37 +02:00

945 lines
29 KiB
JavaScript

const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const { Pool } = require("pg");
const app = express();
const PORT = Number(process.env.PORT || 3000);
const JWT_SECRET = process.env.JWT_SECRET || "elevator-manager-secret";
const DATABASE_URL = process.env.DATABASE_URL || "postgresql://lift_admin:lift_secret@127.0.0.1:5432/lift_manager";
const SALT_ROUNDS = 10;
const MAX_FILES = 8;
const MAX_FILE_SIZE = 10 * 1024 * 1024;
const COOKIE_NAME = "liftToken";
const PUBLIC_DIR = path.join(__dirname, "public");
const DATA_DIR = path.join(__dirname, "data");
const UPLOAD_DIR = path.join(__dirname, "uploads");
const LEGACY_DATA_FILE = path.join(DATA_DIR, "store.json");
const pool = new Pool({
connectionString: DATABASE_URL
});
ensureDirectories();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(PUBLIC_DIR));
const storage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, UPLOAD_DIR),
filename: (_req, file, cb) => {
const extension = path.extname(file.originalname).toLowerCase();
cb(null, `${Date.now()}-${crypto.randomUUID()}${extension}`);
}
});
const upload = multer({
storage,
limits: {
fileSize: MAX_FILE_SIZE,
files: MAX_FILES
},
fileFilter: (_req, file, cb) => {
const isPdf = file.mimetype === "application/pdf" || path.extname(file.originalname).toLowerCase() === ".pdf";
cb(isPdf ? null : new Error("Csak PDF fájl tölthető fel."), isPdf);
}
});
app.get("/", (_req, res) => {
res.sendFile(path.join(PUBLIC_DIR, "index.html"));
});
app.get("/bejelentkezes", requireGuestPage, (_req, res) => {
res.sendFile(path.join(PUBLIC_DIR, "login.html"));
});
app.get("/regisztracio", requireGuestPage, (_req, res) => {
res.sendFile(path.join(PUBLIC_DIR, "register.html"));
});
app.get("/alkalmazas", requirePageAuth, (_req, res) => {
res.sendFile(path.join(PUBLIC_DIR, "app.html"));
});
app.get("/naplo", requirePageAuth, (_req, res) => {
res.sendFile(path.join(PUBLIC_DIR, "logs.html"));
});
app.get("/api/health", async (_req, res) => {
const result = await pool.query("SELECT NOW() AS now");
res.json({ message: "A rendszer működik.", dbTime: result.rows[0].now });
});
app.post("/api/register", async (req, res) => {
try {
const username = req.body.username?.trim().toLowerCase();
const password = req.body.password?.trim();
const fullName = req.body.fullName?.trim();
const routeName = req.body.routeName?.trim();
if (!username || !password || !fullName || !routeName) {
return res.status(400).json({ message: "Minden regisztrációs mező kötelező." });
}
const existingUser = await pool.query("SELECT id FROM users WHERE username = $1", [username]);
if (existingUser.rowCount) {
return res.status(409).json({ message: "Ez a felhasználónév már foglalt." });
}
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
await pool.query(
`INSERT INTO users (username, password_hash, full_name, route_name)
VALUES ($1, $2, $3, $4)`,
[username, passwordHash, fullName, routeName]
);
return res.status(201).json({
message: "Sikeres regisztráció. Most már bejelentkezhetsz.",
redirectPath: "/bejelentkezes"
});
} catch (_error) {
return res.status(500).json({ message: "Hiba történt a regisztráció során." });
}
});
app.post("/api/login", async (req, res) => {
try {
const username = req.body.username?.trim().toLowerCase();
const password = req.body.password?.trim();
if (!username || !password) {
return res.status(400).json({ message: "Felhasználónév és jelszó kötelező." });
}
const result = await pool.query(
`SELECT id, username, password_hash, full_name, route_name
FROM users
WHERE username = $1`,
[username]
);
if (!result.rowCount) {
return res.status(401).json({ message: "Hibás bejelentkezési adatok." });
}
const user = mapUserRow(result.rows[0]);
const passwordMatches = await bcrypt.compare(password, result.rows[0].password_hash);
if (!passwordMatches) {
return res.status(401).json({ message: "Hibás bejelentkezési adatok." });
}
setAuthCookie(res, createToken(user));
return res.json({
message: "Sikeres bejelentkezés.",
redirectPath: "/alkalmazas",
user
});
} catch (_error) {
return res.status(500).json({ message: "Hiba történt a bejelentkezés során." });
}
});
app.post("/api/logout", (_req, res) => {
clearAuthCookie(res);
return res.json({ message: "Sikeres kijelentkezés.", redirectPath: "/bejelentkezes" });
});
app.get("/api/me", authenticateRequest, (req, res) => {
res.json({ user: req.user });
});
app.get("/api/dashboard", authenticateRequest, async (req, res) => {
const result = await pool.query(
`SELECT id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at
FROM elevators
WHERE route_name = $1
AND next_inspection_date IS NOT NULL
AND next_inspection_date >= CURRENT_DATE
AND next_inspection_date <= CURRENT_DATE + INTERVAL '7 days'
ORDER BY next_inspection_date ASC NULLS LAST, id DESC`,
[req.user.routeName]
);
res.json({
routeName: req.user.routeName,
elevators: result.rows.map(mapElevatorRow)
});
});
app.get("/api/elevators", authenticateRequest, async (req, res) => {
const result = await pool.query(
`SELECT id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at
FROM elevators
WHERE route_name = $1
ORDER BY name ASC, id DESC`,
[req.user.routeName]
);
res.json({
routeName: req.user.routeName,
elevators: result.rows.map(mapElevatorRow)
});
});
app.post("/api/elevators", authenticateRequest, async (req, res) => {
const payload = normalizeElevatorPayload(req.body, req.user);
const validationMessage = validateElevatorPayload(payload);
if (validationMessage) {
return res.status(400).json({ message: validationMessage });
}
const result = await pool.query(
`INSERT INTO elevators (
name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at`,
[
payload.name,
payload.serialNumber,
payload.location,
payload.routeName,
payload.lastInspectionDate,
payload.nextInspectionDate,
payload.inspectionIntervalDays,
payload.status
]
);
res.status(201).json({
message: "Az eszköz sikeresen létre lett hozva.",
elevator: mapElevatorRow(result.rows[0])
});
});
app.put("/api/elevators/:elevatorId", authenticateRequest, async (req, res) => {
const elevatorId = Number(req.params.elevatorId);
if (!Number.isInteger(elevatorId) || elevatorId <= 0) {
return res.status(400).json({ message: "Érvénytelen eszközazonosító." });
}
const payload = normalizeElevatorPayload(req.body, req.user);
const validationMessage = validateElevatorPayload(payload);
if (validationMessage) {
return res.status(400).json({ message: validationMessage });
}
const result = await pool.query(
`UPDATE elevators
SET name = $1,
serial_number = $2,
location = $3,
route_name = $4,
last_inspection_date = $5,
next_inspection_date = $6,
inspection_interval_days = $7,
last_status = $8,
updated_at = NOW()
WHERE id = $9 AND route_name = $10
RETURNING id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at`,
[
payload.name,
payload.serialNumber,
payload.location,
payload.routeName,
payload.lastInspectionDate,
payload.nextInspectionDate,
payload.inspectionIntervalDays,
payload.status,
elevatorId,
req.user.routeName
]
);
if (!result.rowCount) {
return res.status(404).json({ message: "A keresett eszköz nem található." });
}
res.json({
message: "Az eszköz adatai frissültek.",
elevator: mapElevatorRow(result.rows[0])
});
});
app.get("/api/logs", authenticateRequest, async (req, res) => {
const search = req.query.search?.trim().toLowerCase() || "";
const sortBy = req.query.sortBy?.trim() || "inspectionDateDesc";
const params = [req.user.routeName];
let whereClause = "WHERE l.route_name = $1";
if (search) {
params.push(`%${search}%`);
whereClause += ` AND LOWER(CONCAT_WS(' ', l.elevator_name, l.serial_number, l.location, l.route_name, l.technician_name, l.notes, l.status)) LIKE $2`;
}
const result = await pool.query(
`SELECT l.id, l.elevator_id, l.elevator_name, l.serial_number, l.location, l.route_name, l.inspection_date,
l.next_inspection_date, l.inspection_interval_days, l.status, l.notes,
l.technician_name, l.created_by_user_id, l.created_at,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'id', lf.id,
'originalName', lf.original_name,
'size', lf.size,
'downloadPath', '/api/logs/' || l.id || '/files/' || lf.id
)
ORDER BY lf.id
) FILTER (WHERE lf.id IS NOT NULL),
'[]'::json
) AS files
FROM logs l
LEFT JOIN log_files lf ON lf.log_id = l.id
${whereClause}
GROUP BY l.id
ORDER BY ${buildLogOrder(sortBy)}`,
params
);
res.json({ logs: result.rows.map(mapLogRow) });
});
app.post("/api/logs", authenticateRequest, upload.array("pdfs", MAX_FILES), async (req, res) => {
const uploadedFiles = req.files || [];
const client = await pool.connect();
try {
const payload = normalizeLogPayload(req.body, req.user);
const validationMessage = validateLogPayload(payload);
if (validationMessage) {
removeUploadedFiles(uploadedFiles);
return res.status(400).json({ message: validationMessage });
}
await client.query("BEGIN");
let elevatorRow = null;
if (payload.elevatorId) {
elevatorRow = await findElevatorById(client, payload.elevatorId, req.user.routeName);
if (!elevatorRow) {
removeUploadedFiles(uploadedFiles);
return res.status(404).json({ message: "A kiválasztott eszköz nem található." });
}
const updatedElevator = await client.query(
`UPDATE elevators
SET last_inspection_date = $1,
next_inspection_date = $2,
inspection_interval_days = $3,
last_status = $4,
updated_at = NOW()
WHERE id = $5
RETURNING id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at`,
[
payload.inspectionDate,
payload.nextInspectionDate,
payload.inspectionIntervalDays,
payload.status,
payload.elevatorId
]
);
elevatorRow = updatedElevator.rows[0];
} else {
const upsertedElevator = await client.query(
`INSERT INTO elevators (
name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (serial_number)
DO UPDATE SET
name = EXCLUDED.name,
location = EXCLUDED.location,
route_name = EXCLUDED.route_name,
last_inspection_date = EXCLUDED.last_inspection_date,
next_inspection_date = EXCLUDED.next_inspection_date,
inspection_interval_days = EXCLUDED.inspection_interval_days,
last_status = EXCLUDED.last_status,
updated_at = NOW()
RETURNING id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at`,
[
payload.elevatorName,
payload.serialNumber,
payload.location,
payload.routeName,
payload.inspectionDate,
payload.nextInspectionDate,
payload.inspectionIntervalDays,
payload.status
]
);
elevatorRow = upsertedElevator.rows[0];
}
const logInsert = await client.query(
`INSERT INTO logs (
elevator_id, elevator_name, serial_number, location, route_name, inspection_date,
next_inspection_date, inspection_interval_days, status, notes,
technician_name, created_by_user_id
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id`,
[
elevatorRow.id,
elevatorRow.name,
elevatorRow.serial_number,
elevatorRow.location,
elevatorRow.route_name,
payload.inspectionDate,
payload.nextInspectionDate,
payload.inspectionIntervalDays,
payload.status,
payload.notes,
req.user.fullName,
req.user.id
]
);
const logId = logInsert.rows[0].id;
for (const file of uploadedFiles) {
const originalName = normalizeUploadedFilename(file.originalname);
await client.query(
`INSERT INTO log_files (log_id, original_name, stored_name, size, mime_type)
VALUES ($1, $2, $3, $4, $5)`,
[logId, originalName, file.filename, file.size, file.mimetype]
);
}
await client.query("COMMIT");
res.status(201).json({
message: "Az ellenőrzési bejegyzés sikeresen mentve lett.",
log: await fetchLogById(logId)
});
} catch (_error) {
await client.query("ROLLBACK");
removeUploadedFiles(uploadedFiles);
res.status(500).json({ message: "Hiba történt a bejegyzés mentése közben." });
} finally {
client.release();
}
});
app.get("/api/logs/:logId/files/:fileId", authenticateRequest, async (req, res) => {
const logId = Number(req.params.logId);
const fileId = Number(req.params.fileId);
const result = await pool.query(
`SELECT stored_name, original_name
FROM log_files
WHERE id = $1 AND log_id = $2`,
[fileId, logId]
);
if (!result.rowCount) {
return res.status(404).json({ message: "A keresett fájl nem található." });
}
const file = result.rows[0];
const absolutePath = path.join(UPLOAD_DIR, file.stored_name);
if (!fs.existsSync(absolutePath)) {
return res.status(404).json({ message: "A fájl már nem érhető el." });
}
return res.download(absolutePath, normalizeUploadedFilename(file.original_name));
});
app.use((error, req, res, _next) => {
removeUploadedFiles(req.files || []);
const isMulterError = error instanceof multer.MulterError;
const message = isMulterError
? "A PDF feltöltés sikertelen volt. Ellenőrizd a fájlméretet és a darabszámot."
: error.message || "Váratlan szerverhiba történt.";
res.status(400).json({ message });
});
startServer();
async function startServer() {
try {
await initializeDatabase();
await migrateLegacyJsonIfNeeded();
app.listen(PORT, () => {
console.log(`A liftellenőrzési kezelő fut a(z) http://localhost:${PORT} címen.`);
});
} catch (error) {
console.error("Nem sikerült elindítani a szervert:", error.message);
process.exit(1);
}
}
function ensureDirectories() {
[DATA_DIR, UPLOAD_DIR, PUBLIC_DIR].forEach((directory) => {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
});
}
async function initializeDatabase() {
await pool.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
full_name TEXT NOT NULL,
route_name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS elevators (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
serial_number TEXT NOT NULL UNIQUE,
location TEXT NOT NULL,
route_name TEXT NOT NULL,
last_inspection_date DATE,
next_inspection_date DATE,
inspection_interval_days INTEGER NOT NULL DEFAULT 30,
last_status TEXT NOT NULL DEFAULT 'rendben',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS logs (
id SERIAL PRIMARY KEY,
elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL,
elevator_name TEXT NOT NULL,
serial_number TEXT NOT NULL,
location TEXT NOT NULL,
route_name TEXT NOT NULL,
inspection_date DATE NOT NULL,
next_inspection_date DATE NOT NULL,
inspection_interval_days INTEGER NOT NULL DEFAULT 30,
status TEXT NOT NULL,
notes TEXT NOT NULL DEFAULT '',
technician_name TEXT NOT NULL,
created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS log_files (
id SERIAL PRIMARY KEY,
log_id INTEGER NOT NULL REFERENCES logs(id) ON DELETE CASCADE,
original_name TEXT NOT NULL,
stored_name TEXT NOT NULL,
size INTEGER NOT NULL,
mime_type TEXT NOT NULL
);
`);
await pool.query(`
ALTER TABLE logs
ADD COLUMN IF NOT EXISTS elevator_id INTEGER REFERENCES elevators(id) ON DELETE SET NULL;
`);
}
async function migrateLegacyJsonIfNeeded() {
if (!fs.existsSync(LEGACY_DATA_FILE)) {
return;
}
const userCount = await pool.query("SELECT COUNT(*)::int AS count FROM users");
if (userCount.rows[0].count > 0) {
return;
}
const legacy = JSON.parse(fs.readFileSync(LEGACY_DATA_FILE, "utf-8"));
const client = await pool.connect();
const userIdMap = new Map();
try {
await client.query("BEGIN");
for (const user of legacy.users || []) {
const result = await client.query(
`INSERT INTO users (username, password_hash, full_name, route_name, created_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id`,
[user.username, user.passwordHash, user.fullName, user.routeName, user.createdAt || new Date().toISOString()]
);
userIdMap.set(user.id, result.rows[0].id);
}
for (const elevator of legacy.elevators || []) {
await client.query(
`INSERT INTO elevators (
name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
elevator.name,
elevator.serialNumber,
elevator.location,
elevator.routeName,
elevator.lastInspectionDate || null,
elevator.nextInspectionDate || null,
elevator.inspectionIntervalDays || 30,
elevator.lastStatus || "rendben",
elevator.updatedAt || new Date().toISOString()
]
);
}
for (const log of legacy.logs || []) {
const logResult = await client.query(
`INSERT INTO logs (
elevator_name, serial_number, location, route_name, inspection_date,
next_inspection_date, inspection_interval_days, status, notes,
technician_name, created_by_user_id, created_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id`,
[
log.elevatorName,
log.serialNumber,
log.location,
log.routeName,
log.inspectionDate,
log.nextInspectionDate,
log.inspectionIntervalDays || 30,
log.status || "rendben",
log.notes || "",
log.technicianName,
userIdMap.get(log.createdByUserId) || null,
log.createdAt || new Date().toISOString()
]
);
for (const file of log.files || []) {
await client.query(
`INSERT INTO log_files (log_id, original_name, stored_name, size, mime_type)
VALUES ($1, $2, $3, $4, $5)`,
[logResult.rows[0].id, file.originalName, file.storedName, file.size || 0, file.mimeType || "application/pdf"]
);
}
}
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
function createToken(user) {
return jwt.sign(user, JWT_SECRET, { expiresIn: "8h" });
}
function setAuthCookie(res, token) {
res.setHeader("Set-Cookie", serializeCookie(COOKIE_NAME, token, {
httpOnly: true,
sameSite: "Lax",
path: "/",
maxAge: 8 * 60 * 60
}));
}
function clearAuthCookie(res) {
res.setHeader("Set-Cookie", serializeCookie(COOKIE_NAME, "", {
httpOnly: true,
sameSite: "Lax",
path: "/",
maxAge: 0
}));
}
function serializeCookie(name, value, options) {
const parts = [`${name}=${encodeURIComponent(value)}`];
parts.push(`Path=${options.path || "/"}`);
parts.push(`Max-Age=${options.maxAge || 0}`);
parts.push("HttpOnly");
parts.push(`SameSite=${options.sameSite || "Lax"}`);
return parts.join("; ");
}
function parseCookies(req) {
const rawCookie = req.headers.cookie || "";
return rawCookie.split(";").reduce((cookies, entry) => {
const [name, ...rest] = entry.trim().split("=");
if (!name) {
return cookies;
}
cookies[name] = decodeURIComponent(rest.join("="));
return cookies;
}, {});
}
function getCookieToken(req) {
return parseCookies(req)[COOKIE_NAME] || "";
}
function getTokenFromRequest(req) {
const headerToken = req.headers.authorization?.startsWith("Bearer ")
? req.headers.authorization.slice(7)
: "";
const queryToken = req.query.token?.trim() || "";
const cookieToken = getCookieToken(req);
return headerToken || cookieToken || queryToken;
}
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (_error) {
return null;
}
}
function requireGuestPage(req, res, next) {
const token = getCookieToken(req);
return token && verifyToken(token) ? res.redirect("/alkalmazas") : next();
}
function requirePageAuth(req, res, next) {
const token = getCookieToken(req);
const decoded = token ? verifyToken(token) : null;
if (!decoded) {
return res.redirect("/bejelentkezes");
}
req.user = decoded;
next();
}
function authenticateRequest(req, res, next) {
const token = getTokenFromRequest(req);
const decoded = token ? verifyToken(token) : null;
if (!decoded) {
return res.status(401).json({ message: "Bejelentkezés szükséges." });
}
req.user = decoded;
next();
}
function normalizeElevatorPayload(body, user) {
const inspectionIntervalDays = Number(body.inspectionIntervalDays || 30);
return {
name: body.name?.trim(),
serialNumber: body.serialNumber?.trim().toUpperCase(),
location: body.location?.trim(),
routeName: user.routeName,
lastInspectionDate: body.lastInspectionDate?.trim() || null,
nextInspectionDate: body.nextInspectionDate?.trim() || null,
inspectionIntervalDays: Number.isFinite(inspectionIntervalDays) ? inspectionIntervalDays : 30,
status: body.status?.trim() || "rendben"
};
}
function validateElevatorPayload(payload) {
if (!payload.name || !payload.serialNumber || !payload.location) {
return "Az eszköz neve, gyári száma és helyszíne kötelező.";
}
if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) {
return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie.";
}
if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) {
return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie.";
}
return "";
}
function normalizeLogPayload(body, user) {
const inspectionIntervalDays = Number(body.inspectionIntervalDays || 30);
return {
elevatorId: Number(body.elevatorId || 0) || null,
elevatorName: body.elevatorName?.trim(),
serialNumber: body.serialNumber?.trim().toUpperCase(),
location: body.location?.trim(),
routeName: body.routeName?.trim() || user.routeName,
inspectionDate: body.inspectionDate?.trim(),
nextInspectionDate: body.nextInspectionDate?.trim(),
inspectionIntervalDays: Number.isFinite(inspectionIntervalDays) ? inspectionIntervalDays : 30,
status: body.status?.trim() || "rendben",
notes: body.notes?.trim() || ""
};
}
function validateLogPayload(payload) {
if (!payload.elevatorId && (!payload.elevatorName || !payload.serialNumber || !payload.location)) {
return "A lift neve, gyári száma és helyszíne kötelező.";
}
if (!payload.inspectionDate || !payload.nextInspectionDate) {
return "Az ellenőrzés és a következő ellenőrzés dátuma kötelező.";
}
if (!Number.isInteger(payload.inspectionIntervalDays) || payload.inspectionIntervalDays < 1) {
return "Az ellenőrzési ciklusnak legalább 1 napnak kell lennie.";
}
return "";
}
function removeUploadedFiles(files) {
files.forEach((file) => {
const absolutePath = path.join(UPLOAD_DIR, file.filename);
if (fs.existsSync(absolutePath)) {
fs.unlinkSync(absolutePath);
}
});
}
function buildLogOrder(sortBy) {
const orderMap = {
inspectionDateAsc: "l.inspection_date ASC, l.id DESC",
inspectionDateDesc: "l.inspection_date DESC, l.id DESC",
nextInspectionAsc: "l.next_inspection_date ASC, l.id DESC",
nextInspectionDesc: "l.next_inspection_date DESC, l.id DESC",
elevatorNameAsc: "l.elevator_name ASC, l.id DESC",
statusAsc: "l.status ASC, l.id DESC"
};
return orderMap[sortBy] || orderMap.inspectionDateDesc;
}
async function fetchLogById(logId) {
const result = await pool.query(
`SELECT l.id, l.elevator_id, l.elevator_name, l.serial_number, l.location, l.route_name, l.inspection_date,
l.next_inspection_date, l.inspection_interval_days, l.status, l.notes,
l.technician_name, l.created_by_user_id, l.created_at,
COALESCE(
JSON_AGG(
JSON_BUILD_OBJECT(
'id', lf.id,
'originalName', lf.original_name,
'size', lf.size,
'downloadPath', '/api/logs/' || l.id || '/files/' || lf.id
)
ORDER BY lf.id
) FILTER (WHERE lf.id IS NOT NULL),
'[]'::json
) AS files
FROM logs l
LEFT JOIN log_files lf ON lf.log_id = l.id
WHERE l.id = $1
GROUP BY l.id`,
[logId]
);
return result.rowCount ? mapLogRow(result.rows[0]) : null;
}
async function findElevatorById(executor, elevatorId, routeName) {
const result = await executor.query(
`SELECT id, name, serial_number, location, route_name, last_inspection_date,
next_inspection_date, inspection_interval_days, last_status, updated_at
FROM elevators
WHERE id = $1 AND route_name = $2`,
[elevatorId, routeName]
);
return result.rowCount ? result.rows[0] : null;
}
function mapUserRow(row) {
return {
id: row.id,
username: row.username,
fullName: row.full_name,
routeName: row.route_name
};
}
function mapElevatorRow(row) {
return {
id: row.id,
name: row.name,
serialNumber: row.serial_number,
location: row.location,
routeName: row.route_name,
lastInspectionDate: row.last_inspection_date,
nextInspectionDate: row.next_inspection_date,
inspectionIntervalDays: row.inspection_interval_days,
lastStatus: row.last_status,
updatedAt: row.updated_at
};
}
function mapLogRow(row) {
return {
id: row.id,
elevatorId: row.elevator_id,
elevatorName: row.elevator_name,
serialNumber: row.serial_number,
location: row.location,
routeName: row.route_name,
inspectionDate: row.inspection_date,
nextInspectionDate: row.next_inspection_date,
inspectionIntervalDays: row.inspection_interval_days,
status: row.status,
notes: row.notes,
technicianName: row.technician_name,
createdByUserId: row.created_by_user_id,
createdAt: row.created_at,
files: Array.isArray(row.files)
? row.files.map((file) => ({
...file,
originalName: normalizeUploadedFilename(file.originalName)
}))
: []
};
}
function normalizeUploadedFilename(value) {
if (!value || !containsLikelyFilenameMojibake(value)) {
return value;
}
const decoded = Buffer.from(value, "latin1").toString("utf8").normalize("NFC");
return containsReplacementCharacter(decoded) ? value : decoded;
}
function containsLikelyFilenameMojibake(value) {
return /[\u00C2-\u00C5\u00CC-\u00CF\u00D0-\u00D6\u00D8-\u00DE\u0080-\u009F]/.test(value);
return /[ÃÅÂ]/.test(value);
}
function containsReplacementCharacter(value) {
return value.includes("\uFFFD");
}