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:
+21
-12
@@ -24,9 +24,10 @@ function loadBrand(){
|
||||
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
||||
function pad(n){return String(n).padStart(2,'0')}
|
||||
function toDate(v){if(!v)return null;const d=new Date(v);return isNaN(d.getTime())?null:d}
|
||||
function fmtDate(v){const d=toDate(v);return d?pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+d.getFullYear():''}
|
||||
function fmtShort(v){const d=toDate(v);return d?pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+String(d.getFullYear()).slice(2):''}
|
||||
function fmtLong(v){const d=toDate(v);return d?d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'}):''}
|
||||
function dtIsDateOnly(v){return typeof v==='string'&&/^\d{4}-\d{2}-\d{2}/.test(v)}
|
||||
function fmtDate(v){return dtIsDateOnly(v)?fmtDateOnlyIso(v):(v?fmtDateFull(v):'')}
|
||||
function fmtShort(v){return dtIsDateOnly(v)?fmtShortIso(v):(v?fmtDateShort(v):'')}
|
||||
function fmtLong(v){return dtIsDateOnly(v)?fmtDateOnlyLongIso(v):(v?fmtDateLong(v):'')}
|
||||
function plural(n,one,few,many){const m10=n%10,m100=n%100;if(m100>=11&&m100<=14)return many;if(m10===1)return one;if(m10>=2&&m10<=4)return few;return many}
|
||||
function words(n,one,few,many){return n+' '+plural(n,one,few,many)}
|
||||
function isImageFile(name){return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name||'')}
|
||||
@@ -41,14 +42,19 @@ function hideEl(el,on){if(el)el.classList.toggle('rep-hidden',!!on)}
|
||||
function node(html){const t=document.createElement('template');t.innerHTML=html.trim();return t.content.firstElementChild}
|
||||
function periodLabel(from,to){
|
||||
if(!from&&!to)return '';
|
||||
const a=toDate(from),b=toDate(to);
|
||||
if(a&&b)return fmtDate(a)+' — '+fmtDate(b);
|
||||
return fmtDate(a)||fmtDate(b);
|
||||
if(from&&to)return fmtDate(from)+' — '+fmtDate(to);
|
||||
return fmtDate(from)||fmtDate(to);
|
||||
}
|
||||
function dtIsoYear(v){
|
||||
if(!v)return null;
|
||||
if(dtIsDateOnly(v))return String(v).slice(0,4);
|
||||
const d=toDate(v);
|
||||
if(!d)return null;
|
||||
try{return new Intl.DateTimeFormat('en-CA',{timeZone:DateTime.tz,year:'numeric'}).format(d).slice(0,4)}catch{return String(d.getFullYear())}
|
||||
}
|
||||
function yearLabel(from,to){
|
||||
const a=toDate(from),b=toDate(to);
|
||||
if(!a&&!b)return '';
|
||||
const ay=a?a.getFullYear():null,by=b?b.getFullYear():null;
|
||||
if(!from&&!to)return '';
|
||||
const ay=dtIsoYear(from),by=dtIsoYear(to);
|
||||
if(ay&&by)return ay===by?String(ay):ay+' — '+String(by).slice(2);
|
||||
return String(ay||by);
|
||||
}
|
||||
@@ -268,12 +274,15 @@ function statSep(variant){
|
||||
const extra=variant===1?' list__item--u-ip5wdmggw':(variant===2?' list__item--u-ixebr6v8q':'');
|
||||
return '<div class="list__item list__item--u-isd3lg3iz'+extra+'"></div>';
|
||||
}
|
||||
function dtYearMonth(d){
|
||||
try{return new Intl.DateTimeFormat('en-CA',{timeZone:DateTime.tz,year:'numeric',month:'2-digit'}).format(d).slice(0,7)}catch{return d.getFullYear()+'-'+d.getMonth()}
|
||||
}
|
||||
function renderAbout(data,pool){
|
||||
const entries=data.entries||[];
|
||||
const photos=(data.photos||[]).length;
|
||||
const files=entries.reduce(function(n,e){return n+((e.files||[]).length)},0);
|
||||
const months={};
|
||||
entries.forEach(function(e){const d=toDate(e.created_at);if(d)months[d.getFullYear()+'-'+d.getMonth()]=1});
|
||||
entries.forEach(function(e){const d=toDate(e.created_at);if(d)months[dtYearMonth(d)]=1});
|
||||
const photosTotal=entries.filter(function(e){return e.photo_path}).length+photos;
|
||||
setText('aboutTitle',(data.name&&String(data.name).trim())||'Отчёт');
|
||||
setText('aboutLead',(data.message&&String(data.message).trim())||('Мы собрали '+words(entries.length,'запись','записи','записей')+' о работе за этот период — с фотографиями, файлами и описанием каждого занятия.'));
|
||||
@@ -461,7 +470,7 @@ function renderFooter(data,heading,pool){
|
||||
const label=btn.querySelector('.text-block-wrap-div');
|
||||
if(label)label.textContent='Перейти по ссылке';
|
||||
}
|
||||
setText('footCopy',state.systemName+' © '+new Date().getFullYear());
|
||||
setText('footCopy',state.systemName+' © '+nowYear());
|
||||
setText('footBrand',heading);
|
||||
const fbox=document.getElementById('footerPhotoBox');
|
||||
const fimg=document.getElementById('footerPhoto');
|
||||
@@ -563,5 +572,5 @@ function bindEvents(){
|
||||
|
||||
bindEvents();
|
||||
makeNavLinks();
|
||||
loadBrand().then(function(){loadReport()});
|
||||
initDateTime().then(function(){return loadBrand()}).then(function(){loadReport()});
|
||||
function closeLightbox(){const lb=document.getElementById('lightbox');if(lb)lb.classList.remove('is-open')}
|
||||
Reference in New Issue
Block a user