Reworked calendar form
Build and Publish / Build and Publish Docker Image (push) Successful in 2m17s

This commit is contained in:
Ultradesu
2026-05-18 17:07:43 +03:00
parent 90fd4f86f8
commit 43441ee430
6 changed files with 462 additions and 168 deletions
Generated
+101 -1
View File
@@ -29,6 +29,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -1574,6 +1583,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "matchit"
version = "0.8.4"
@@ -1649,6 +1667,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
@@ -2093,6 +2120,23 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "reqwest"
version = "0.12.28"
@@ -2376,6 +2420,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -2724,6 +2777,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.47"
@@ -3012,6 +3074,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -3113,6 +3205,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -3255,7 +3353,7 @@ dependencies = [
[[package]]
name = "web-petting"
version = "0.1.8"
version = "0.1.9"
dependencies = [
"chrono",
"chrono-tz",
@@ -3268,6 +3366,8 @@ dependencies = [
"serde_html_form",
"serde_json",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
]
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "web-petting"
version = "0.1.9"
version = "0.1.10"
edition = "2024"
[dependencies]
@@ -17,3 +17,4 @@ futures = "0.3"
tokio = { version = "1", features = ["fs"] }
uuid = { version = "1", features = ["v4"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+3
View File
@@ -274,6 +274,7 @@ struct ScheduleNewTemplate<'a> {
clients: Vec<Client>,
users: Vec<User>,
current_user_id: i64,
timezone: String,
}
#[derive(Debug, Template)]
@@ -1110,6 +1111,7 @@ async fn schedule_new_page(
let current_user_id = get_admin_id(&session).await.unwrap_or(0);
let clients = query!(Client, $status == "active").all(&db).await?;
let users = query!(User, $status == "active").all(&db).await?;
let tz = crate::tz::load_tz(&db).await;
let body = ScheduleNewTemplate {
t: lang.t(),
lang,
@@ -1117,6 +1119,7 @@ async fn schedule_new_page(
clients,
users,
current_user_id,
timezone: tz.to_string(),
}
.render()?;
html_response(body, lang)
+2 -2
View File
@@ -400,7 +400,7 @@ static RU: Translations = Translations {
schedule_new_title: "Запланировать визиты",
schedule_client: "Клиент",
schedule_admin: "Исполнитель",
schedule_default_time: "Время по умолчанию",
schedule_default_time: "Время",
schedule_time_start: "С",
schedule_time_end: "До",
schedule_pick_dates: "Добавить дату",
@@ -603,7 +603,7 @@ static EN: Translations = Translations {
schedule_new_title: "Plan Visits",
schedule_client: "Client",
schedule_admin: "Assigned to",
schedule_default_time: "Default Time",
schedule_default_time: "Time",
schedule_time_start: "From",
schedule_time_end: "To",
schedule_pick_dates: "Add date",
+7
View File
@@ -6,6 +6,8 @@ mod public;
mod telegram;
mod tz;
use tracing_subscriber;
use cot::cli::CliMetadata;
use cot::config::{
DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, SessionStoreConfig,
@@ -97,5 +99,10 @@ impl Project for PettingProject {
#[cot::main]
fn main() -> impl Project {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.try_init();
PettingProject
}
+300 -117
View File
@@ -10,6 +10,7 @@
<div class="form-card">
<form method="post" action="/admin/schedule/create" id="visitForm">
<!-- Client -->
<div class="field">
<label class="label">{{ t.schedule_client }}</label>
@@ -44,57 +45,40 @@
<!-- Default time -->
<div class="field">
<label class="label">{{ t.schedule_default_time }}</label>
<div class="columns is-mobile" style="margin-bottom:0;">
<div class="column">
<div class="control">
<div class="time-row">
<div class="time-block">
<span class="time-lbl">{{ t.schedule_time_start }}</span>
<input class="input" type="time" id="defaultStart" value="18:00">
</div>
</div>
<div class="column">
<div class="control">
<div class="time-sep"></div>
<div class="time-block">
<span class="time-lbl">{{ t.schedule_time_end }}</span>
<input class="input" type="time" id="defaultEnd" value="19:00">
</div>
</div>
</div>
<!-- Calendar -->
<div class="field">
<div class="cal-toolbar">
<button type="button" id="rangeModeBtn" class="button is-small is-outlined is-info">⇔ Выбрать диапазон</button>
<button type="button" id="resetBtn" class="button is-small is-outlined is-danger" style="display:none;">✕ Сбросить</button>
</div>
<!-- Add individual date -->
<div class="field">
<label class="label">{{ t.schedule_pick_dates }}</label>
<div class="columns is-mobile" style="margin-bottom:0;">
<div class="column">
<div class="control">
<input class="input" type="date" id="pickDate">
</div>
</div>
<div class="column is-narrow">
<button type="button" class="button is-info" id="addDateBtn">+</button>
<div class="sched-cal">
<div class="cal-nav">
<button type="button" id="calPrev"></button>
<span id="calTitle"></span>
<button type="button" id="calNext"></button>
</div>
<div class="cal-grid" id="calGrid"></div>
</div>
</div>
<!-- Date range fill -->
<div class="field">
<label class="label is-small has-text-grey">{{ t.schedule_range_from }} — {{ t.schedule_range_to }}</label>
<div class="columns is-mobile" style="margin-bottom:0;">
<div class="column">
<input class="input" type="date" id="rangeFrom">
</div>
<div class="column">
<input class="input" type="date" id="rangeTo">
</div>
<div class="column is-narrow">
<button type="button" class="button is-info is-outlined" id="fillRangeBtn">{{ t.schedule_fill_range }}</button>
</div>
</div>
</div>
<!-- Selected days list -->
<div class="field">
<label class="label">{{ t.schedule_selected_days }}</label>
<div id="daysList">
<p class="has-text-grey is-size-7" id="noDaysMsg">{{ t.schedule_no_days }}</p>
</div>
<!-- Selected days -->
<div class="field" id="selectedSection" style="display:none;">
<label class="label">{{ t.schedule_selected_days }} <span id="selectedCount" class="tag is-info is-light" style="margin-left:0.4rem;"></span></label>
<div id="daysList"></div>
</div>
<!-- Notes -->
@@ -105,29 +89,114 @@
</div>
</div>
<!-- Hidden days data -->
<input type="hidden" name="days_json" id="daysJson" value="[]">
<button type="submit" class="button is-primary is-fullwidth" id="submitBtn" disabled>{{ t.schedule_create }}</button>
</form>
</div>
<style>
.day-row {
display: flex; align-items: center; gap: 0.4rem; padding: 0.4rem 0;
border-bottom: 1px solid #f0f0f0; flex-wrap: wrap;
/* Time row */
.time-row { display:flex; align-items:center; gap:0.5rem; }
.time-block { display:flex; flex-direction:column; flex:1; }
.time-lbl { font-size:0.75rem; color:#888; margin-bottom:0.2rem; }
.time-sep { font-size:1.2rem; color:#aaa; padding-top:1.2rem; }
/* Calendar toolbar */
.cal-toolbar { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.5rem; }
/* Calendar container */
.sched-cal { background:#fafafa; border:1px solid #eee; border-radius:10px; overflow:hidden; }
.cal-nav { display:flex; align-items:center; justify-content:space-between; padding:0.6rem 0.75rem; background:#fff; border-bottom:1px solid #eee; }
.cal-nav button { background:none; border:none; font-size:1.1rem; cursor:pointer; color:#6c63ff; padding:0.2rem 0.5rem; border-radius:4px; }
.cal-nav button:hover { background:#f0eeff; }
.cal-nav span { font-weight:700; font-size:1rem; color:#333; }
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); }
.cal-wday { text-align:center; font-size:0.72rem; font-weight:700; color:#aaa; padding:0.4rem 0; background:#fafafa; }
.cal-wday.is-weekend { color:#f0a0a0; }
.cal-day { text-align:center; padding:0.55rem 0.2rem; font-size:0.9rem; cursor:pointer; color:#333; border-radius:0; transition:background 0.1s; position:relative; user-select:none; -webkit-user-select:none; }
.cal-day:hover { background:#f0eeff; }
.cal-day.is-empty { cursor:default; }
.cal-day.is-empty:hover { background:none; }
.cal-day.is-today { font-weight:700; color:#6c63ff; }
.cal-day.is-selected { background:#6c63ff !important; color:#fff !important; border-radius:0; }
.cal-day.is-range-start { background:#a89cff !important; color:#fff !important; }
.cal-day.is-past { color:#ccc; }
/* Selected days list */
.day-row { display:flex; align-items:center; gap:0.4rem; padding:0.45rem 0; border-bottom:1px solid #f5f5f5; }
.day-row:last-child { border-bottom:none; }
.day-date { font-weight:600; font-size:0.85rem; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.day-times { display:flex; align-items:center; gap:0.25rem; flex-shrink:0; }
.day-times .time-sep { color:#bbb; font-size:0.8rem; }
.day-rm { background:none; border:none; color:#ccc; cursor:pointer; font-size:1rem; padding:0.15rem 0.25rem; flex-shrink:0; line-height:1; }
.day-rm:hover { color:#e55; }
/* Time badge */
.time-badge-wrap {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
background: #ede9ff;
color: #5b52d6;
border: 1.5px solid #c4beff;
border-radius: 20px;
padding: 0.15rem 0.5rem;
font-size: 0.78rem;
font-weight: 700;
cursor: pointer;
min-width: 3.6rem;
white-space: nowrap;
}
.time-badge-wrap:focus-within {
border-color: #6c63ff;
background: #f0eeff;
}
.time-badge-label { pointer-events: none; z-index: 1; }
.time-badge-input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
border: none;
background: transparent;
padding: 0;
margin: 0;
font-size: 16px; /* prevent iOS zoom */
}
.day-row .day-date { font-weight: 600; min-width: 6rem; font-size: 0.9rem; }
.day-row input[type="time"] { width: 7rem; padding: 0.2rem 0.4rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.85rem; }
.day-row .remove-btn { color: #e55; cursor: pointer; font-size: 0.8rem; margin-left: auto; background: none; border: none; }
</style>
<script>
const days = new Map(); // date string -> {start, end}
const removeLabel = '{{ t.schedule_remove_day }}';
const weekdays = '{{ lang.code() }}' === 'ru'
? ['Вс','Пн','Вт','Ср','Чт','Пт','Сб']
: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
(function() {
const IS_RU = '{{ lang.code() }}' === 'ru';
const TZ = '{{ timezone }}';
const WDAYS = IS_RU ? ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'] : ['Mo','Tu','We','Th','Fr','Sa','Su'];
const MONTHS = IS_RU
? ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь']
: ['January','February','March','April','May','June','July','August','September','October','November','December'];
const days = new Map(); // dateStr -> {start, end}
let viewYear, viewMonth;
let rangeMode = false;
let rangeStart = null; // dateStr of first tap in range mode
const now = new Date();
const todayStr_ = tzDateStr(now);
viewYear = parseInt(todayStr_.slice(0, 4));
viewMonth = parseInt(todayStr_.slice(5, 7)) - 1; // 0-based
function isoDate(y, m, d) {
return y + '-' + String(m+1).padStart(2,'0') + '-' + String(d).padStart(2,'0');
}
function getDefaults() {
return {
@@ -136,111 +205,225 @@ function getDefaults() {
};
}
function addDay(dateStr) {
if (!dateStr || days.has(dateStr)) return;
function addDay(ds) {
if (!ds || days.has(ds)) return;
const def = getDefaults();
days.set(dateStr, { start: def.start, end: def.end });
renderDays();
days.set(ds, { start: def.start, end: def.end });
}
function removeDay(dateStr) {
days.delete(dateStr);
renderDays();
function toggleDay(ds) {
if (days.has(ds)) { days.delete(ds); } else { addDay(ds); }
}
function renderDays() {
// Получить текущую дату в нужном TZ как строку YYYY-MM-DD
function tzDateStr(d) {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit'
}).formatToParts(d);
const p = {};
parts.forEach(function(x) { p[x.type] = x.value; });
return p.year + '-' + p.month + '-' + p.day;
}
function fillRange(from, to) {
if (from > to) { let t = from; from = to; to = t; }
// Используем полдень чтобы избежать проблем с переходом суток при смене DST
let cur = new Date(from + 'T12:00:00');
const end = new Date(to + 'T12:00:00');
while (cur <= end) {
addDay(tzDateStr(cur));
cur.setDate(cur.getDate() + 1);
}
}
// ── Render calendar ──────────────────────────────────────
function renderCal() {
document.getElementById('calTitle').textContent = MONTHS[viewMonth] + ' ' + viewYear;
const grid = document.getElementById('calGrid');
grid.innerHTML = '';
// Weekday headers
WDAYS.forEach(function(wd, i) {
const cell = document.createElement('div');
cell.className = 'cal-wday' + (i >= 5 ? ' is-weekend' : '');
cell.textContent = wd;
grid.appendChild(cell);
});
// First day of month (Mon=0 for our grid)
const first = new Date(viewYear, viewMonth, 1);
let startDow = first.getDay(); // 0=Sun
startDow = (startDow === 0) ? 6 : startDow - 1; // shift to Mon=0
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
const todayStr = todayStr_;
// Empty cells before first day
for (let i = 0; i < startDow; i++) {
const empty = document.createElement('div');
empty.className = 'cal-day is-empty';
grid.appendChild(empty);
}
for (let d = 1; d <= daysInMonth; d++) {
const ds = isoDate(viewYear, viewMonth, d);
const cell = document.createElement('div');
let cls = 'cal-day';
if (ds === todayStr) cls += ' is-today';
if (days.has(ds)) cls += ' is-selected';
if (ds === rangeStart) cls += ' is-range-start';
cell.className = cls;
cell.textContent = d;
cell.dataset.date = ds;
cell.addEventListener('click', onDayClick);
grid.appendChild(cell);
}
}
function onDayClick(e) {
const ds = e.currentTarget.dataset.date;
if (!ds) return;
if (rangeMode) {
if (days.has(ds)) {
days.delete(ds);
if (rangeStart === ds) rangeStart = null;
} else if (!rangeStart) {
rangeStart = ds;
} else {
fillRange(rangeStart, ds);
rangeStart = null;
}
} else {
toggleDay(ds);
}
renderCal();
renderList();
}
// ── Render selected days list ────────────────────────────
function renderList() {
const list = document.getElementById('daysList');
const msg = document.getElementById('noDaysMsg');
const section = document.getElementById('selectedSection');
const count = document.getElementById('selectedCount');
const btn = document.getElementById('submitBtn');
// Remove old day rows
list.querySelectorAll('.day-row').forEach(el => el.remove());
list.innerHTML = '';
const resetBtn = document.getElementById('resetBtn');
if (days.size === 0) {
msg.style.display = '';
section.style.display = 'none';
btn.disabled = true;
document.getElementById('daysJson').value = '[]';
resetBtn.style.display = 'none';
return;
}
msg.style.display = 'none';
btn.disabled = false;
resetBtn.style.display = '';
section.style.display = '';
btn.disabled = false;
count.textContent = days.size;
// Sort by date
const sorted = [...days.entries()].sort((a,b) => a[0].localeCompare(b[0]));
sorted.forEach(([dateStr, times]) => {
const d = new Date(dateStr + 'T00:00:00');
const wd = weekdays[d.getDay()];
const label = dateStr.split('-').reverse().join('.') + ' ' + wd;
sorted.forEach(function([ds, times]) {
const d = new Date(ds + 'T00:00:00');
const dow = IS_RU
? ['Вс','Пн','Вт','Ср','Чт','Пт','Сб'][d.getDay()]
: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()];
const parts = ds.split('-');
const label = parts[2] + '.' + parts[1] + ' ' + dow; // DD.MM Вт
const row = document.createElement('div');
row.className = 'day-row';
row.innerHTML = `
<span class="day-date">${label}</span>
<input type="time" value="${times.start}" data-date="${dateStr}" data-field="start">
<span>—</span>
<input type="time" value="${times.end}" data-date="${dateStr}" data-field="end">
<button type="button" class="remove-btn" data-date="${dateStr}">${removeLabel}</button>
`;
row.innerHTML =
'<span class="day-date">' + label + '</span>' +
'<div class="day-times">' +
'<div class="time-badge-wrap">' +
'<span class="time-badge-label">' + times.start + '</span>' +
'<input type="time" class="time-badge-input" value="' + times.start + '" data-date="' + ds + '" data-field="start">' +
'</div>' +
'<span class="time-sep">—</span>' +
'<div class="time-badge-wrap">' +
'<span class="time-badge-label">' + times.end + '</span>' +
'<input type="time" class="time-badge-input" value="' + times.end + '" data-date="' + ds + '" data-field="end">' +
'</div>' +
'</div>' +
'<button type="button" class="day-rm" data-date="' + ds + '" title="Убрать">✕</button>';
list.appendChild(row);
});
// Update hidden JSON
updateJson();
// Bind events
list.querySelectorAll('input[type="time"]').forEach(inp => {
list.querySelectorAll('.time-badge-input').forEach(function(inp) {
inp.addEventListener('change', function() {
const dt = this.dataset.date;
const field = this.dataset.field;
if (days.has(dt)) {
days.get(dt)[field] = this.value;
const d = days.get(this.dataset.date);
if (d) {
d[this.dataset.field] = this.value;
this.previousElementSibling.textContent = this.value;
updateJson();
}
});
});
list.querySelectorAll('.remove-btn').forEach(btn => {
btn.addEventListener('click', function() {
removeDay(this.dataset.date);
list.querySelectorAll('.day-rm').forEach(function(b) {
b.addEventListener('click', function() {
days.delete(this.dataset.date);
renderCal();
renderList();
});
});
updateJson();
}
function updateJson() {
const arr = [...days.entries()].map(([date, t]) => ({
date: date,
time_start: t.start,
time_end: t.end
}));
const arr = [...days.entries()].map(function([date, t]) {
return { date: date, time_start: t.start, time_end: t.end };
});
document.getElementById('daysJson').value = JSON.stringify(arr);
}
document.getElementById('addDateBtn').addEventListener('click', function() {
const v = document.getElementById('pickDate').value;
addDay(v);
document.getElementById('pickDate').value = '';
// ── Navigation ───────────────────────────────────────────
document.getElementById('calPrev').addEventListener('click', function() {
viewMonth--;
if (viewMonth < 0) { viewMonth = 11; viewYear--; }
renderCal();
});
document.getElementById('calNext').addEventListener('click', function() {
viewMonth++;
if (viewMonth > 11) { viewMonth = 0; viewYear++; }
renderCal();
});
document.getElementById('pickDate').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); document.getElementById('addDateBtn').click(); }
});
document.getElementById('fillRangeBtn').addEventListener('click', function() {
const from = document.getElementById('rangeFrom').value;
const to = document.getElementById('rangeTo').value;
if (!from || !to || from > to) return;
let cur = new Date(from + 'T00:00:00');
const end = new Date(to + 'T00:00:00');
while (cur <= end) {
const ds = cur.toISOString().slice(0, 10);
addDay(ds);
cur.setDate(cur.getDate() + 1);
// ── Range mode toggle ────────────────────────────────────
document.getElementById('rangeModeBtn').addEventListener('click', function() {
rangeMode = !rangeMode;
rangeStart = null;
if (rangeMode) {
this.classList.remove('is-outlined', 'is-info');
this.classList.add('is-warning');
this.textContent = '✕ Выбрать отдельные дни';
} else {
this.classList.remove('is-warning');
this.classList.add('is-outlined', 'is-info');
this.textContent = '⇔ Выбрать диапазон';
}
document.getElementById('rangeFrom').value = '';
document.getElementById('rangeTo').value = '';
renderCal();
});
// Set default pick date to today
document.getElementById('pickDate').valueAsDate = new Date();
document.getElementById('resetBtn').addEventListener('click', function() {
days.clear();
rangeStart = null;
renderCal();
renderList();
});
// ── Default time change → update existing days ───────────
// (only updates days that still have the old default)
// kept simple: doesn't retroactively update already-added days
renderCal();
renderList();
})();
</script>
{% endblock %}