Reworked calendar form
Build and Publish / Build and Publish Docker Image (push) Successful in 2m17s
Build and Publish / Build and Publish Docker Image (push) Successful in 2m17s
This commit is contained in:
+347
-164
@@ -9,238 +9,421 @@
|
||||
</div>
|
||||
|
||||
<div class="form-card">
|
||||
<form method="post" action="/admin/schedule/create" id="visitForm">
|
||||
<!-- Client -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_client }}</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="client_id" required>
|
||||
<option value="">—</option>
|
||||
{% for c in &clients %}
|
||||
<option value="{{ c.id }}">{{ c.name }}{% if let Some(p) = c.phone.as_deref() %} ({{ p }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<form method="post" action="/admin/schedule/create" id="visitForm">
|
||||
|
||||
<!-- Client -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_client }}</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="client_id" required>
|
||||
<option value="">—</option>
|
||||
{% for c in &clients %}
|
||||
<option value="{{ c.id }}">{{ c.name }}{% if let Some(p) = c.phone.as_deref() %} ({{ p }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_admin }}</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="user_id">
|
||||
{% for u in &users %}
|
||||
<option value="{{ u.id }}" {% if u.id.unwrap() == current_user_id %}selected{% endif %}>
|
||||
{{ u.display_name.as_deref().unwrap_or(&u.login) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Admin -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_admin }}</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="user_id">
|
||||
{% for u in &users %}
|
||||
<option value="{{ u.id }}" {% if u.id.unwrap() == current_user_id %}selected{% endif %}>
|
||||
{{ u.display_name.as_deref().unwrap_or(&u.login) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<input class="input" type="time" id="defaultStart" value="18:00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="control">
|
||||
<input class="input" type="time" id="defaultEnd" value="19:00">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Default time -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_default_time }}</label>
|
||||
<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 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>
|
||||
|
||||
<!-- 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>
|
||||
</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>
|
||||
|
||||
<!-- 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 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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_notes }}</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="notes" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="field">
|
||||
<label class="label">{{ t.schedule_notes }}</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="notes" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
</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;
|
||||
}
|
||||
.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; }
|
||||
/* 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 */
|
||||
}
|
||||
</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 {
|
||||
start: document.getElementById('defaultStart').value || '18:00',
|
||||
end: document.getElementById('defaultEnd').value || '19:00'
|
||||
end: document.getElementById('defaultEnd').value || '19:00'
|
||||
};
|
||||
}
|
||||
|
||||
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() {
|
||||
const list = document.getElementById('daysList');
|
||||
const msg = document.getElementById('noDaysMsg');
|
||||
const btn = document.getElementById('submitBtn');
|
||||
// Получить текущую дату в нужном 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;
|
||||
}
|
||||
|
||||
// Remove old day rows
|
||||
list.querySelectorAll('.day-row').forEach(el => el.remove());
|
||||
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 section = document.getElementById('selectedSection');
|
||||
const count = document.getElementById('selectedCount');
|
||||
const btn = document.getElementById('submitBtn');
|
||||
|
||||
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';
|
||||
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]));
|
||||
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 %}
|
||||
|
||||
Reference in New Issue
Block a user