Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Компактный 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);
|
||||
Reference in New Issue
Block a user