316 lines
9.0 KiB
JavaScript
316 lines
9.0 KiB
JavaScript
/**
|
|
* Компактный Date Range Picker для /reports.
|
|
* Сохраняет #date-from / #date-to для совместимости с остальным кодом.
|
|
*/
|
|
(function (global) {
|
|
const MONTHS = [
|
|
"янв",
|
|
"фев",
|
|
"мар",
|
|
"апр",
|
|
"май",
|
|
"июн",
|
|
"июл",
|
|
"авг",
|
|
"сен",
|
|
"окт",
|
|
"ноя",
|
|
"дек",
|
|
];
|
|
|
|
let draftFrom = null;
|
|
let draftTo = null;
|
|
let viewMonth = null;
|
|
let activePreset = "day";
|
|
let panelEl = null;
|
|
let triggerEl = null;
|
|
|
|
function pad(n) {
|
|
return String(n).padStart(2, "0");
|
|
}
|
|
|
|
function toIso(d) {
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
}
|
|
|
|
function parseIso(s) {
|
|
if (!s) return null;
|
|
const p = String(s).split("-");
|
|
if (p.length !== 3) return null;
|
|
const d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2]));
|
|
d.setHours(0, 0, 0, 0);
|
|
return d;
|
|
}
|
|
|
|
function startOfDay(d) {
|
|
const x = new Date(d);
|
|
x.setHours(0, 0, 0, 0);
|
|
return x;
|
|
}
|
|
|
|
function today() {
|
|
return startOfDay(new Date());
|
|
}
|
|
|
|
function sameDay(a, b) {
|
|
return a && b && toIso(a) === toIso(b);
|
|
}
|
|
|
|
function formatLabel(from, to) {
|
|
if (!from || !to) return "Выберите даты";
|
|
const t = today();
|
|
const fmt = (d) => `${pad(d.getDate())}.${pad(d.getMonth() + 1)}`;
|
|
if (sameDay(from, to) && sameDay(from, t)) return "Сегодня";
|
|
if (sameDay(from, to)) return fmt(from);
|
|
return `${fmt(from)} — ${fmt(to)}`;
|
|
}
|
|
|
|
function syncHiddenInputs(from, to) {
|
|
const fromEl = document.getElementById("date-from");
|
|
const toEl = document.getElementById("date-to");
|
|
if (fromEl) fromEl.value = from ? toIso(from) : "";
|
|
if (toEl) toEl.value = to ? toIso(to) : "";
|
|
}
|
|
|
|
function readCommittedRange() {
|
|
const fromEl = document.getElementById("date-from");
|
|
const toEl = document.getElementById("date-to");
|
|
return {
|
|
from: parseIso(fromEl?.value),
|
|
to: parseIso(toEl?.value),
|
|
};
|
|
}
|
|
|
|
function updateTriggerLabel() {
|
|
const label = document.getElementById("reportsDateRangeLabel");
|
|
if (!label) return;
|
|
const { from, to } = readCommittedRange();
|
|
label.textContent = formatLabel(from, to);
|
|
}
|
|
|
|
function setPresetActive(preset) {
|
|
activePreset = preset || "";
|
|
document.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
|
btn.classList.toggle("active", btn.getAttribute("data-drp-preset") === activePreset);
|
|
});
|
|
}
|
|
|
|
function computePresetRange(preset) {
|
|
const t = today();
|
|
let from = t;
|
|
let to = t;
|
|
if (preset === "week") {
|
|
from = new Date(t);
|
|
from.setDate(t.getDate() - ((t.getDay() + 6) % 7));
|
|
} else if (preset === "month") {
|
|
from = new Date(t.getFullYear(), t.getMonth(), 1);
|
|
}
|
|
return { from: startOfDay(from), to: startOfDay(to) };
|
|
}
|
|
|
|
function commitDraft() {
|
|
if (draftFrom && !draftTo) draftTo = draftFrom;
|
|
if (!draftFrom || !draftTo) return readCommittedRange();
|
|
syncHiddenInputs(draftFrom, draftTo);
|
|
setPresetActive("");
|
|
updateTriggerLabel();
|
|
return { from: draftFrom, to: draftTo };
|
|
}
|
|
|
|
function applyRange(from, to, preset) {
|
|
syncHiddenInputs(from, to);
|
|
draftFrom = from;
|
|
draftTo = to;
|
|
setPresetActive(preset || "");
|
|
updateTriggerLabel();
|
|
}
|
|
|
|
function commitSelection() {
|
|
commitDraft();
|
|
closePanel();
|
|
}
|
|
|
|
function positionPanel() {
|
|
if (!triggerEl || !panelEl || panelEl.hidden) return;
|
|
const rect = triggerEl.getBoundingClientRect();
|
|
const width = Math.max(rect.width, 304);
|
|
let left = rect.left;
|
|
if (left + width > window.innerWidth - 12) {
|
|
left = Math.max(12, window.innerWidth - width - 12);
|
|
}
|
|
let top = rect.bottom + 6;
|
|
const panelHeight = panelEl.offsetHeight || 360;
|
|
if (top + panelHeight > window.innerHeight - 12) {
|
|
top = Math.max(12, rect.top - panelHeight - 6);
|
|
}
|
|
panelEl.style.left = `${left}px`;
|
|
panelEl.style.top = `${top}px`;
|
|
panelEl.style.width = `${width}px`;
|
|
}
|
|
|
|
function closePanel() {
|
|
if (panelEl) panelEl.hidden = true;
|
|
if (triggerEl) triggerEl.setAttribute("aria-expanded", "false");
|
|
window.removeEventListener("resize", positionPanel);
|
|
window.removeEventListener("scroll", positionPanel, true);
|
|
}
|
|
|
|
function openPanel() {
|
|
const committed = readCommittedRange();
|
|
draftFrom = committed.from || today();
|
|
draftTo = committed.to || draftFrom;
|
|
viewMonth = new Date(draftFrom.getFullYear(), draftFrom.getMonth(), 1);
|
|
renderCalendar();
|
|
if (panelEl) panelEl.hidden = false;
|
|
if (triggerEl) triggerEl.setAttribute("aria-expanded", "true");
|
|
positionPanel();
|
|
window.addEventListener("resize", positionPanel);
|
|
window.addEventListener("scroll", positionPanel, true);
|
|
}
|
|
|
|
function togglePanel() {
|
|
if (panelEl?.hidden) openPanel();
|
|
else closePanel();
|
|
}
|
|
|
|
function inRange(d, a, b) {
|
|
if (!a || !b) return false;
|
|
const t = d.getTime();
|
|
const lo = Math.min(a.getTime(), b.getTime());
|
|
const hi = Math.max(a.getTime(), b.getTime());
|
|
return t >= lo && t <= hi;
|
|
}
|
|
|
|
function onDayClick(d) {
|
|
if (!draftFrom || (draftFrom && draftTo)) {
|
|
draftFrom = d;
|
|
draftTo = null;
|
|
setPresetActive("");
|
|
renderCalendar();
|
|
return;
|
|
}
|
|
if (d < draftFrom) {
|
|
draftTo = draftFrom;
|
|
draftFrom = d;
|
|
} else {
|
|
draftTo = d;
|
|
}
|
|
setPresetActive("");
|
|
commitSelection();
|
|
}
|
|
|
|
function renderCalendar() {
|
|
const grid = document.getElementById("reportsDateRangeGrid");
|
|
const title = document.getElementById("reportsDateRangeMonth");
|
|
if (!grid || !viewMonth) return;
|
|
|
|
const y = viewMonth.getFullYear();
|
|
const m = viewMonth.getMonth();
|
|
if (title) title.textContent = `${MONTHS[m]} ${y}`;
|
|
|
|
const first = new Date(y, m, 1);
|
|
const startOffset = (first.getDay() + 6) % 7;
|
|
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
|
|
|
let html = "";
|
|
for (let i = 0; i < startOffset; i++) {
|
|
html += '<span class="reports-drp__day reports-drp__day--empty"></span>';
|
|
}
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
const d = new Date(y, m, day);
|
|
const classes = ["reports-drp__day"];
|
|
if (sameDay(d, today())) classes.push("reports-drp__day--today");
|
|
if (draftFrom && sameDay(d, draftFrom)) classes.push("reports-drp__day--edge");
|
|
if (draftTo && sameDay(d, draftTo)) classes.push("reports-drp__day--edge");
|
|
if (inRange(d, draftFrom, draftTo)) classes.push("reports-drp__day--in-range");
|
|
html += `<button type="button" class="${classes.join(" ")}" data-drp-day="${toIso(d)}">${day}</button>`;
|
|
}
|
|
grid.innerHTML = html;
|
|
|
|
grid.querySelectorAll("[data-drp-day]").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const d = parseIso(btn.getAttribute("data-drp-day"));
|
|
if (d) onDayClick(d);
|
|
});
|
|
});
|
|
}
|
|
|
|
function setPeriod(period) {
|
|
const { from, to } = computePresetRange(period);
|
|
applyRange(from, to, period);
|
|
closePanel();
|
|
}
|
|
|
|
function init(options) {
|
|
triggerEl = document.getElementById("reportsDateRangeTrigger");
|
|
panelEl = document.getElementById("reportsDateRangePanel");
|
|
if (!triggerEl || !panelEl) return;
|
|
|
|
if (panelEl.parentElement !== document.body) {
|
|
document.body.appendChild(panelEl);
|
|
}
|
|
|
|
panelEl.addEventListener("mousedown", (e) => e.stopPropagation());
|
|
panelEl.addEventListener("click", (e) => e.stopPropagation());
|
|
|
|
document.querySelectorAll("[data-drp-preset]").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
setPeriod(btn.getAttribute("data-drp-preset") || "day");
|
|
});
|
|
});
|
|
|
|
document.getElementById("reportsDateRangePrev")?.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
if (!viewMonth) viewMonth = today();
|
|
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1);
|
|
renderCalendar();
|
|
positionPanel();
|
|
});
|
|
document.getElementById("reportsDateRangeNext")?.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
if (!viewMonth) viewMonth = today();
|
|
viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1);
|
|
renderCalendar();
|
|
positionPanel();
|
|
});
|
|
|
|
triggerEl.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
togglePanel();
|
|
});
|
|
|
|
document.addEventListener("click", () => {
|
|
if (!panelEl.hidden) closePanel();
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") closePanel();
|
|
});
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const highlightReportId = urlParams.get("report");
|
|
if highlightReportId) {
|
|
global.__wespHighlightReportId = highlightReportId;
|
|
setPeriod("month");
|
|
} else {
|
|
setPeriod("month");
|
|
}
|
|
|
|
if (options?.onReady) options.onReady();
|
|
}
|
|
|
|
global.WespReportsDateRange = {
|
|
init,
|
|
setPeriod,
|
|
commit: commitDraft,
|
|
updateTriggerLabel,
|
|
formatLabel,
|
|
parseIso,
|
|
toIso,
|
|
};
|
|
|
|
global.setPeriod = setPeriod;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|