feat(datetime): timezone/time_format в настройках + единый хелпер дат
Часовой пояс и формат времени (24h/12h) перенесены из браузера в настройки приложения: валидация IANA-зоны, сид в db/init.sql + db/migration.sql, отдача в GET /api/public-settings, раздел #sec-datetime в настройках с живым превью. Границы суток в SQL переведены на tzDayStart/tzDayEnd + bindTz ($TZ$) — раньше $n::date по TIMESTAMPTZ считал дни в UTC (у контейнера TimeZone=UTC) и молча сдвигал выборку на день; хардкод Europe/Moscow вычищен, now() заменён на tzWall(). Новый public/js/datetime.js: три семейства хелперов (instant / чистая DATE-строка без Date() / чистая TIME-строка), подключён на всех страницах включая публичные share/report/index, initDateTime() встроен в checkAuth(). Прямые toLocale*/getFullYear/toISOString().slice(0,10) в public/ выпилены. Попутно: bindTz добавляет параметр только при наличии $TZ$ в SQL (иначе запрос виснет вечно без global error handler) — регрессия закрыта в api.smoketest.js; починены off-by-one месяца в report.js и группировка по дате в share.js.
This commit is contained in:
@@ -1088,6 +1088,35 @@ async function trashPurgeDays() {
|
||||
return Number.isFinite(v) && v >= 1 && v <= 3650 ? v : 30;
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEZONE = (() => {
|
||||
const fromEnv = String(process.env.TZ || '').trim();
|
||||
return validTimezone(fromEnv) ? fromEnv : 'Europe/Moscow';
|
||||
})();
|
||||
|
||||
function validTimezone(tz) {
|
||||
if (!tz || typeof tz !== 'string') return false;
|
||||
try { new Intl.DateTimeFormat('ru-RU', { timeZone: tz }); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
async function appTimezone() {
|
||||
const v = String(await getSetting('timezone', DEFAULT_TIMEZONE) || '').trim();
|
||||
return validTimezone(v) ? v : DEFAULT_TIMEZONE;
|
||||
}
|
||||
|
||||
async function appHour12() {
|
||||
return (await getSetting('time_format', '24h')) === '12h';
|
||||
}
|
||||
|
||||
function tzDayStart(idx) { return `($${idx}::date::timestamp AT TIME ZONE $TZ$)`; }
|
||||
function tzDayEnd(idx) { return `($${idx}::date::timestamp + interval '1 day') AT TIME ZONE $TZ$`; }
|
||||
function tzWall() { return `(now() AT TIME ZONE $TZ$)`; }
|
||||
|
||||
function bindTz(sql, params, tz) {
|
||||
if (!sql.includes('$TZ$')) return { sql, params };
|
||||
params.push(tz);
|
||||
return { sql: sql.replace(/\$TZ\$/g, '$' + params.length), params };
|
||||
}
|
||||
|
||||
async function purgeScheduledDeletions() {
|
||||
const erefs = await pool.query('SELECT id FROM entries WHERE purge_at IS NOT NULL AND purge_at <= now()');
|
||||
for (const r of erefs.rows) {
|
||||
@@ -1780,8 +1809,8 @@ app.get('/api/settings', requireAdmin, async (_, res) => {
|
||||
|
||||
app.get('/api/public-settings', apiLimiter, async (_, res) => {
|
||||
const out = await cacheWrap('public-settings', PUBLIC_TTL_MS, async () => {
|
||||
const keys = ['system_name', 'system_logo', 'footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos', 'cookie_notice_text', 'spam_interval_min', 'photo_capture_resolution', 'photo_capture_quality', 'photo_enhance_engine', 'camera_enabled', 'photo_ai_face_mode', 'photo_ai_face_model', 'photo_ai_device_pref'];
|
||||
const defaults = { system_name: 'WhatIDo', system_logo: '', spam_interval_min: '30', photo_capture_resolution: '640x480', photo_capture_quality: '0.92', photo_enhance_engine: 'auto', camera_enabled: 'true', photo_ai_face_mode: PHOTO_AI_DEFAULT_FACE_MODE, photo_ai_face_model: PHOTO_AI_FACE_MODEL, photo_ai_device_pref: 'auto' };
|
||||
const keys = ['system_name', 'system_logo', 'footer_left', 'footer_right', 'share_show_student_message', 'share_show_entry_date', 'share_show_student_names', 'share_show_group_photos', 'cookie_notice_text', 'spam_interval_min', 'photo_capture_resolution', 'photo_capture_quality', 'photo_enhance_engine', 'camera_enabled', 'photo_ai_face_mode', 'photo_ai_face_model', 'photo_ai_device_pref', 'timezone', 'time_format'];
|
||||
const defaults = { system_name: 'WhatIDo', system_logo: '', spam_interval_min: '30', photo_capture_resolution: '640x480', photo_capture_quality: '0.92', photo_enhance_engine: 'auto', camera_enabled: 'true', photo_ai_face_mode: PHOTO_AI_DEFAULT_FACE_MODE, photo_ai_face_model: PHOTO_AI_FACE_MODEL, photo_ai_device_pref: 'auto', timezone: DEFAULT_TIMEZONE, time_format: '24h' };
|
||||
const result = {};
|
||||
for (const k of keys) result[k] = await getSetting(k, defaults[k] || '');
|
||||
const rm = /^(\d{2,5})x(\d{2,5})$/.exec(result.photo_capture_resolution);
|
||||
@@ -1859,6 +1888,12 @@ app.put('/api/settings', requireAdmin, async (req, res) => {
|
||||
if (key === 'lesson_ai_prompt' && String(value).length > 8000) {
|
||||
return res.status(400).json({ error: 'lesson_ai_prompt длиннее 8000 символов' });
|
||||
}
|
||||
if (key === 'timezone' && !validTimezone(String(value).trim())) {
|
||||
return res.status(400).json({ error: 'timezone должен быть корректным часовым поясом IANA, например Europe/Moscow' });
|
||||
}
|
||||
if (key === 'time_format' && !['24h', '12h'].includes(String(value))) {
|
||||
return res.status(400).json({ error: 'time_format должен быть 24h или 12h' });
|
||||
}
|
||||
if (key.startsWith('notify_') && key !== 'notify_retention_days' && !['true', 'false'].includes(String(value))) {
|
||||
return res.status(400).json({ error: `${key} должен быть true или false` });
|
||||
}
|
||||
@@ -2771,10 +2806,10 @@ app.get('/api/share/:token', fileLimiter, async (req, res) => {
|
||||
const params = [];
|
||||
if (l.group_id) { params.push(l.group_id); conditions.push(`e.group_id = $${params.length}`); }
|
||||
if (l.student_name) { params.push(l.student_name); conditions.push(`e.student_name = $${params.length}`); }
|
||||
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
||||
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= ${tzDayStart(params.length)}`); }
|
||||
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ${tzDayEnd(params.length)}`); }
|
||||
conditions.push('e.deleted_at IS NULL');
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
||||
const where = conditions.length ? ' WHERE ' + bindTz(conditions.join(' AND '), params, await appTimezone()).sql : '';
|
||||
const { rows: entries } = await pool.query(
|
||||
`SELECT e.*, g.name AS group_name FROM entries e
|
||||
JOIN groups g ON g.id = e.group_id${where} ORDER BY e.created_at DESC`,
|
||||
@@ -2857,14 +2892,15 @@ app.get('/api/share/:shareToken/files/:fileToken', fileLimiter, async (req, res)
|
||||
const params = [];
|
||||
if (l.group_id) { params.push(l.group_id); conditions.push(`e.group_id = $${params.length}`); }
|
||||
if (l.student_name) { params.push(l.student_name); conditions.push(`e.student_name = $${params.length}`); }
|
||||
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
||||
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||
if (l.date_from) { params.push(l.date_from); conditions.push(`e.created_at >= ${tzDayStart(params.length)}`); }
|
||||
if (l.date_to) { params.push(l.date_to); conditions.push(`e.created_at < ${tzDayEnd(params.length)}`); }
|
||||
params.push(fileToken);
|
||||
const bound = bindTz(conditions.join(' AND '), params, await appTimezone());
|
||||
const { rows } = await pool.query(
|
||||
`SELECT pf.path, pf.name FROM project_files pf
|
||||
JOIN entries e ON e.id = pf.entry_id
|
||||
WHERE pf.token = $${params.length} AND ${conditions.join(' AND ')}`,
|
||||
params
|
||||
WHERE pf.token = $${params.length - 1} AND ${bound.sql}`,
|
||||
bound.params
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Not found' });
|
||||
const r = rows[0];
|
||||
@@ -2920,10 +2956,10 @@ app.get('/api/groups/active', apiLimiter, async (_, res) => {
|
||||
AND day_of_week IS NOT NULL
|
||||
AND time_start IS NOT NULL
|
||||
AND time_end IS NOT NULL
|
||||
AND day_of_week = EXTRACT(DOW FROM (now() AT TIME ZONE 'Europe/Moscow'))::int
|
||||
AND (now() AT TIME ZONE 'Europe/Moscow')::time BETWEEN time_start AND time_end
|
||||
AND day_of_week = EXTRACT(DOW FROM (now() AT TIME ZONE $1))::int
|
||||
AND (now() AT TIME ZONE $1)::time BETWEEN time_start AND time_end
|
||||
ORDER BY id
|
||||
`);
|
||||
`, [await appTimezone()]);
|
||||
return rows;
|
||||
});
|
||||
res.json(rows);
|
||||
@@ -4101,9 +4137,9 @@ app.put('/api/students/:id/photos/:pid/main', requireAuth, async (req, res) => {
|
||||
res.json({ ok: true, photo_path: rows[0].photo_path });
|
||||
});
|
||||
|
||||
function fmtLongDate(iso) {
|
||||
function fmtLongDate(iso, tz) {
|
||||
if (!iso) return '';
|
||||
return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric', timeZone: validTimezone(tz) ? tz : DEFAULT_TIMEZONE });
|
||||
}
|
||||
|
||||
app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
@@ -4141,18 +4177,18 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
const bw = branchWhere(req.user, 'g');
|
||||
if (dateFrom) {
|
||||
params.push(dateFrom);
|
||||
conds.push(`e.created_at >= $${params.length}::date`);
|
||||
conds.push(`e.created_at >= ${tzDayStart(params.length)}`);
|
||||
}
|
||||
if (dateTo) {
|
||||
params.push(dateTo);
|
||||
conds.push(`e.created_at < ($${params.length}::date + interval '1 day')`);
|
||||
conds.push(`e.created_at < ${tzDayEnd(params.length)}`);
|
||||
}
|
||||
if (bw.params.length) {
|
||||
const start = params.length + 1;
|
||||
conds.push(`g.branch_id IN (${bw.params.map((_, i) => '$' + (start + i)).join(',')})`);
|
||||
params.push(...bw.params);
|
||||
}
|
||||
const condStr = conds.join(' AND ');
|
||||
const condStr = bindTz(conds.join(' AND '), params, await appTimezone()).sql;
|
||||
const whereStr = ' WHERE ' + condStr;
|
||||
const [studRes, entriesRes, photosRes, mainsRes, filesRes, modulesRes, groupPhotosRes] = await Promise.all([
|
||||
pool.query(
|
||||
@@ -4343,7 +4379,8 @@ app.get('/api/export/student', requireAuth, async (req, res) => {
|
||||
|
||||
let period = null;
|
||||
if (hasPeriod) {
|
||||
period = `Период: ${dateFrom ? fmtLongDate(dateFrom + 'T00:00:00') : 'начало'} — ${dateTo ? fmtLongDate(dateTo + 'T00:00:00') : 'сегодня'}`;
|
||||
const exportTz = await appTimezone();
|
||||
period = `Период: ${dateFrom ? fmtLongDate(dateFrom + 'T00:00:00', exportTz) : 'начало'} — ${dateTo ? fmtLongDate(dateTo + 'T00:00:00', exportTz) : 'сегодня'}`;
|
||||
}
|
||||
|
||||
const reportData = {
|
||||
@@ -4438,13 +4475,13 @@ app.get('/api/groups/:id/export/files', requireAuth, async (req, res) => {
|
||||
const params = [gid];
|
||||
if (dateFrom) {
|
||||
params.push(dateFrom);
|
||||
conds.push(`e.created_at >= $${params.length}::date`);
|
||||
conds.push(`e.created_at >= ${tzDayStart(params.length)}`);
|
||||
}
|
||||
if (dateTo) {
|
||||
params.push(dateTo);
|
||||
conds.push(`e.created_at < ($${params.length}::date + interval '1 day')`);
|
||||
conds.push(`e.created_at < ${tzDayEnd(params.length)}`);
|
||||
}
|
||||
const condStr = conds.join(' AND ');
|
||||
const condStr = bindTz(conds.join(' AND '), params, await appTimezone()).sql;
|
||||
const [studentsRes, filesRes, photosRes, entryPhotosRes, originalsRes] = await Promise.all([
|
||||
pool.query('SELECT id, name FROM students WHERE group_id = $1 ORDER BY name', [gid]),
|
||||
includeFiles
|
||||
@@ -4602,8 +4639,8 @@ app.get('/api/entries', requireAuth, async (req, res) => {
|
||||
else conditions.push('e.deleted_at IS NULL');
|
||||
if (group_id) { params.push(group_id); conditions.push(`e.group_id = $${params.length}`); }
|
||||
if (module_id) { params.push(module_id); conditions.push(`e.module_id = $${params.length}`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`e.created_at >= ${tzDayStart(params.length)}`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`e.created_at < ${tzDayEnd(params.length)}`); }
|
||||
if (student_name) { params.push(student_name); conditions.push(`e.student_name = $${params.length}`); }
|
||||
if (search) { params.push(`%${search}%`); conditions.push(`(e.student_name ILIKE $${params.length} OR e.description ILIKE $${params.length})`); }
|
||||
if (req.user.role !== 'admin') {
|
||||
@@ -4618,7 +4655,7 @@ app.get('/api/entries', requireAuth, async (req, res) => {
|
||||
conditions.push(`g.branch_id IN (${ph})`);
|
||||
}
|
||||
}
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
||||
const where = conditions.length ? ' WHERE ' + bindTz(conditions.join(' AND '), params, await appTimezone()).sql : '';
|
||||
const { rows: crows } = await pool.query(
|
||||
`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id${where}`,
|
||||
params
|
||||
@@ -4838,8 +4875,8 @@ app.get('/api/files', requireAuth, async (req, res) => {
|
||||
if (search) { params.push(`%${search}%`); conditions.push(`pf.name ILIKE $${params.length}`); }
|
||||
if (student_name) { params.push(student_name); conditions.push(`e.student_name = $${params.length}`); }
|
||||
if (group_id) { params.push(group_id); conditions.push(`e.group_id = $${params.length}`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`e.created_at >= $${params.length}::date`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`e.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`e.created_at >= ${tzDayStart(params.length)}`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`e.created_at < ${tzDayEnd(params.length)}`); }
|
||||
if (req.user.role !== 'admin') {
|
||||
if (group_id && !(await groupBelongsToBranches(req.user, group_id))) {
|
||||
return res.status(403).json({ error: 'Нет доступа к этой группе' });
|
||||
@@ -4852,7 +4889,7 @@ app.get('/api/files', requireAuth, async (req, res) => {
|
||||
conditions.push(`g.branch_id IN (${ph})`);
|
||||
}
|
||||
}
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
||||
const where = conditions.length ? ' WHERE ' + bindTz(conditions.join(' AND '), params, await appTimezone()).sql : '';
|
||||
const { rows: crows } = await pool.query(
|
||||
`SELECT count(*)::int AS n FROM project_files pf JOIN entries e ON e.id = pf.entry_id JOIN groups g ON g.id = e.group_id${where}`,
|
||||
params
|
||||
@@ -4975,8 +5012,8 @@ app.get('/api/photos', requireAuth, async (req, res) => {
|
||||
if (search) { params.push(`%${search}%`); conditions.push(`t.title ILIKE $${params.length}`); }
|
||||
if (student_name) { params.push(student_name); conditions.push(`t.student_name = $${params.length}`); }
|
||||
if (group_id) { params.push(group_id); conditions.push(`t.group_id = $${params.length}`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`t.created_at >= $${params.length}::date`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`t.created_at < ($${params.length}::date + interval '1 day')`); }
|
||||
if (date_from) { params.push(date_from); conditions.push(`t.created_at >= ${tzDayStart(params.length)}`); }
|
||||
if (date_to) { params.push(date_to); conditions.push(`t.created_at < ${tzDayEnd(params.length)}`); }
|
||||
if (req.user.role !== 'admin') {
|
||||
if (group_id && !(await groupBelongsToBranches(req.user, group_id))) {
|
||||
return res.status(403).json({ error: 'Нет доступа к этой группе' });
|
||||
@@ -4989,7 +5026,7 @@ app.get('/api/photos', requireAuth, async (req, res) => {
|
||||
conditions.push(`t.branch_id IN (${ph})`);
|
||||
}
|
||||
}
|
||||
const where = conditions.length ? ' WHERE ' + conditions.join(' AND ') : '';
|
||||
const where = conditions.length ? ' WHERE ' + bindTz(conditions.join(' AND '), params, await appTimezone()).sql : '';
|
||||
const from = `FROM (
|
||||
SELECT 'entry'::text AS source_type, 'Главное фото записи'::text AS source_label,
|
||||
e.photo_path AS path, e.id AS source_id, e.student_name, e.group_id,
|
||||
@@ -5053,6 +5090,7 @@ app.get('/api/photos', requireAuth, async (req, res) => {
|
||||
|
||||
app.get('/api/stats', requireAuth, async (req, res) => {
|
||||
const payload = await cacheWrap('stats:' + scopeKey(req.user), STATS_TTL_MS, async () => {
|
||||
const tz = await appTimezone();
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const s = branchScope(req.user);
|
||||
let groupFilter;
|
||||
@@ -5076,7 +5114,7 @@ app.get('/api/stats', requireAuth, async (req, res) => {
|
||||
pool.query(`SELECT count(*)::int AS n FROM groups g${gTrash('visible')}`, groupsParams),
|
||||
pool.query(`SELECT count(*)::int AS n FROM groups g${groupsWhere}`, groupsParams),
|
||||
pool.query(`SELECT count(DISTINCT e.student_name)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL${groupFilter.where}`, groupFilter.params),
|
||||
pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL AND e.created_at >= now()::date${groupFilter.where}`, groupFilter.params),
|
||||
(() => { const p = groupFilter.params.slice(); p.push(tz); return pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL AND e.created_at >= ${tzWall()}::date${groupFilter.where}`.replace(/\$TZ\$/g, '$' + p.length), p); })(),
|
||||
pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.purge_at IS NOT NULL${groupFilter.where}`, groupFilter.params),
|
||||
pool.query(`SELECT count(*)::int AS n FROM groups g${gTrash('pending')}`, groupsParams),
|
||||
]);
|
||||
@@ -5197,6 +5235,7 @@ app.get('/api/system-info', requireAdmin, async (_, res) => {
|
||||
app.get('/api/dashboard', requireAuth, async (req, res) => {
|
||||
const payload = await cacheWrap('dashboard:' + scopeKey(req.user), STATS_TTL_MS, async () => {
|
||||
const DAYS = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
|
||||
const tz = await appTimezone();
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const s = branchScope(req.user);
|
||||
const branchIds = isAdmin ? [] : s.ids;
|
||||
@@ -5213,24 +5252,24 @@ app.get('/api/dashboard', requireAuth, async (req, res) => {
|
||||
pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NOT NULL${ew ? whereEntry : ''}`, ew ? whereGroupParams : []),
|
||||
pool.query(`SELECT count(*)::int AS n FROM groups g${isAdmin ? '' : (branchIds.length ? ` WHERE g.branch_id IN (${branchIds.map((_, i) => `$${i + 1}`).join(',')})` : ' WHERE 1 = 0')}`, isAdmin ? [] : branchIds),
|
||||
pool.query(`SELECT count(DISTINCT e.student_name)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL${ew ? whereEntry : ''}`, ew ? whereGroupParams : []),
|
||||
pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL AND e.created_at >= now()::date${ew ? whereEntry : ''}`, ew ? whereGroupParams : []),
|
||||
(() => { const p = (ew ? whereGroupParams : []).slice(); p.push(tz); return pool.query(`SELECT count(*)::int AS n FROM entries e JOIN groups g ON g.id = e.group_id WHERE e.deleted_at IS NULL AND e.created_at >= ${tzWall()}::date${ew ? whereEntry : ''}`.replace(/\$TZ\$/g, '$' + p.length), p); })(),
|
||||
]);
|
||||
return { entries: entries.rows[0].n, trash: trash.rows[0].n, groups: groups.rows[0].n, students: students.rows[0].n, today: today.rows[0].n };
|
||||
})(),
|
||||
pool.query(
|
||||
`SELECT to_char(e.created_at, 'YYYY-MM-DD') AS d, count(*)::int AS n
|
||||
`SELECT to_char(e.created_at AT TIME ZONE $TZ$, 'YYYY-MM-DD') AS d, count(*)::int AS n
|
||||
FROM entries e JOIN groups g ON g.id = e.group_id
|
||||
WHERE e.deleted_at IS NULL AND e.created_at >= (now() - interval '13 days')::date${whereEntry}
|
||||
GROUP BY 1 ORDER BY 1`,
|
||||
whereEntry ? whereGroupParams : []
|
||||
WHERE e.deleted_at IS NULL AND e.created_at >= (${tzWall()} - interval '13 days')::date${whereEntry}
|
||||
GROUP BY 1 ORDER BY 1`.replace(/\$TZ\$/g, '$' + (whereEntry ? whereGroupParams.length : 0) + 1),
|
||||
(whereEntry ? whereGroupParams : []).concat([tz])
|
||||
),
|
||||
pool.query(
|
||||
`SELECT g.* FROM groups g
|
||||
WHERE g.day_of_week IS NOT NULL AND g.time_start IS NOT NULL AND g.time_end IS NOT NULL
|
||||
AND g.day_of_week = EXTRACT(DOW FROM (now() AT TIME ZONE 'Europe/Moscow'))::int
|
||||
AND (now() AT TIME ZONE 'Europe/Moscow')::time BETWEEN g.time_start AND g.time_end${whereGroup}
|
||||
ORDER BY g.id`,
|
||||
whereGroup ? whereGroupParams : []
|
||||
AND g.day_of_week = EXTRACT(DOW FROM ${tzWall()})::int
|
||||
AND ${tzWall()}::time BETWEEN g.time_start AND g.time_end${whereGroup}
|
||||
ORDER BY g.id`.replace(/\$TZ\$/g, '$' + (whereGroup ? whereGroupParams.length : 0) + 1),
|
||||
(whereGroup ? whereGroupParams : []).concat([tz])
|
||||
),
|
||||
pool.query(
|
||||
`SELECT e.id, e.student_name, e.photo_path, e.created_at, g.name AS group_name
|
||||
|
||||
Reference in New Issue
Block a user