- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import { type Express } from "express";
|
|
import { query } from "./db-client";
|
|
import { getTableConfig } from "./table-config";
|
|
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
|
import * as cache from "./cache";
|
|
|
|
export function registerTimeseriesRoutes(app: Express): void {
|
|
app.get("/api/income/tree-timeseries", authenticateToken, requirePermission('finance.manage'), async (req: AuthenticatedRequest, res) => {
|
|
await handleTreeTimeseries(req, res, "income");
|
|
});
|
|
app.get("/api/expenses/tree-timeseries", authenticateToken, requirePermission('finance.manage'), async (req: AuthenticatedRequest, res) => {
|
|
await handleTreeTimeseries(req, res, "expense");
|
|
});
|
|
}
|
|
|
|
async function handleTreeTimeseries(req: AuthenticatedRequest, res: any, type: "income" | "expense") {
|
|
try {
|
|
const config = getTableConfig();
|
|
const dateFrom = req.query.dateFrom as string;
|
|
const dateTo = req.query.dateTo as string;
|
|
const groupBy = (req.query.groupBy as string) || "month";
|
|
const categoryId = req.query.categoryId as string | undefined;
|
|
|
|
const cacheKey = `${type}:timeseries:${dateFrom}:${dateTo}:${groupBy}:${categoryId}`;
|
|
const cached = cache.get(cacheKey);
|
|
if (cached) { res.json(cached); return; }
|
|
|
|
const table = type === "income" ? config.incomeTable : config.expenseTable;
|
|
const sumCol = type === "income" ? config.incomeSumColumn : config.expenseSumColumn;
|
|
|
|
const dateFilter = [];
|
|
const params: any[] = [];
|
|
if (dateFrom) { dateFilter.push(`"data" >= $${params.length + 1}`); params.push(dateFrom); }
|
|
if (dateTo) { dateFilter.push(`"data" <= $${params.length + 1}`); params.push(dateTo); }
|
|
const where = dateFilter.length > 0 ? `WHERE ${dateFilter.join(" AND ")}` : "";
|
|
|
|
const dateTrunc = groupBy === "year" ? "year" : groupBy === "quarter" ? "quarter" : "month";
|
|
|
|
const result = await query(
|
|
`SELECT DATE_TRUNC('${dateTrunc}', "data"::date) as period, SUM("${sumCol}") as total, COUNT(*) as count
|
|
FROM "${table}" ${where}
|
|
GROUP BY period
|
|
ORDER BY period`,
|
|
params
|
|
);
|
|
|
|
const periods = result.rows.map((r: any) => {
|
|
const d = new Date(r.period);
|
|
if (groupBy === "year") return `${d.getFullYear()}`;
|
|
if (groupBy === "quarter") return `${d.getFullYear()}-Q${Math.floor(d.getMonth() / 3) + 1}`;
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
});
|
|
|
|
const data = result.rows.map((r: any) => parseFloat(r.total) || 0);
|
|
|
|
const response = {
|
|
success: true,
|
|
periods,
|
|
series: [{
|
|
id: "total",
|
|
name: type === "income" ? "Доходы" : "Расходы",
|
|
data,
|
|
}],
|
|
};
|
|
cache.set(cacheKey, response);
|
|
res.json(response);
|
|
} catch (err: any) {
|
|
res.status(500).json({ success: false, error: err.message });
|
|
}
|
|
}
|