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:
+5
-6
@@ -67,7 +67,7 @@ async function loadGroups() {
|
||||
<button class="btn-link" data-export-urls="${g.id}" data-group-name="${esc(g.name)}" title="Скачать .url файлы учеников группы">URL</button>
|
||||
<button class="btn-link" data-export-files="${g.id}" data-group-name="${esc(g.name)}" title="Скачать ZIP-архив файлов группы за период">ZIP</button>
|
||||
</div>
|
||||
<div class="sched-status">${schedSet ? `${DAYS[g.day_of_week]} ${g.time_start.slice(0,5)}–${g.time_end.slice(0,5)}` : 'Не настроено'}</div>
|
||||
<div class="sched-status">${schedSet ? `${DAYS[g.day_of_week]} ${fmtHmStr(g.time_start)}–${fmtHmStr(g.time_end)}` : 'Не настроено'}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') + `</div>`;
|
||||
@@ -162,11 +162,11 @@ async function loadGroupPhotos() {
|
||||
|
||||
function goPhotoPage(p) { photoPage = p; loadGroupPhotos(); }
|
||||
|
||||
function fmtPhotoDate(d) { return d ? d.split('-').reverse().join('.') : ''; }
|
||||
function fmtPhotoDate(d) { return fmtDateOnlyIso(d); }
|
||||
|
||||
function photoHTML(p) {
|
||||
const taken = p.taken_at ? `Дата: ${fmtPhotoDate(p.taken_at)}` : '';
|
||||
const uploaded = `Загружено: ${new Date(p.created_at).toLocaleDateString('ru')}`;
|
||||
const uploaded = `Загружено: ${fmtDateFull(p.created_at)}`;
|
||||
const isCover = currentCover && p.photo_path === currentCover;
|
||||
return `
|
||||
<div class="photo-item" data-photo-id="${p.id}" data-photo-path="${esc(p.photo_path)}" draggable="true">
|
||||
@@ -274,7 +274,7 @@ async function exportGroup(id, name) {
|
||||
e.student_name,
|
||||
e.group_name,
|
||||
e.description.replace(/\n/g, ' '),
|
||||
new Date(e.created_at).toLocaleString('ru'),
|
||||
fmtFull(e.created_at),
|
||||
`${location.origin}/?nameInput=${encodeURIComponent(e.student_name)}`
|
||||
]);
|
||||
});
|
||||
@@ -318,8 +318,7 @@ function closeGroupFilesModal() {
|
||||
}
|
||||
|
||||
function todayISOLocal() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
return todayIso();
|
||||
}
|
||||
|
||||
const GROUP_FILES_PHOTO_BOXES = ['groupFilesMainPhoto', 'groupFilesGallery', 'groupFilesOriginals'];
|
||||
|
||||
Reference in New Issue
Block a user