Files
verstak/contrib/plugins/calendar/panels/calendar.html
T

987 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #1a1a2e;
--bg2: #22223a;
--bg3: #2a2a44;
--text: #e0e0e0;
--text-dim: #888;
--text-bright: #fff;
--border: #333;
--accent: #6366f1;
--accent-hover: #818cf8;
--danger: #ef4444;
--success: #22c55e;
--warn: #f59e0b;
--radius: 8px;
--radius-sm: 4px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
line-height: 1.5;
overflow-x: hidden;
}
/* Header */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
gap: 8px;
flex-wrap: wrap;
}
.header-left { display: flex; align-items: center; gap: 8px; }
.header-title { font-size: 1.1rem; font-weight: 600; color: var(--text-bright); }
.nav-btn {
background: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
padding: 6px 12px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 14px;
transition: background 0.15s;
}
.nav-btn:hover { background: var(--accent); color: #fff; }
.nav-btn.today-btn { font-weight: 600; }
.view-tabs { display: flex; gap: 2px; }
.view-tab {
padding: 6px 14px;
background: var(--bg3);
border: 1px solid var(--border);
color: var(--text-dim);
cursor: pointer;
font-size: 13px;
transition: all 0.15s;
}
.view-tab:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.view-tab:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
.view-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.view-tab:hover:not(.active) { background: var(--bg3); color: var(--text); }
/* Month grid */
.month-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: var(--border);
margin: 0;
}
.day-header {
background: var(--bg3);
padding: 8px 4px;
text-align: center;
font-size: 12px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
}
.day-cell {
background: var(--bg2);
min-height: 90px;
padding: 4px 6px;
cursor: pointer;
transition: background 0.1s;
position: relative;
}
.day-cell:hover { background: var(--bg3); }
.day-cell.other-month { opacity: 0.35; }
.day-cell.today { background: rgba(99, 102, 241, 0.12); }
.day-cell.selected { background: rgba(99, 102, 241, 0.2); box-shadow: inset 0 0 0 1px var(--accent); }
.day-cell.drop-target { background: rgba(34, 197, 94, 0.15); box-shadow: inset 0 0 0 2px var(--success); }
.day-number {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
color: var(--text);
}
.day-cell.today .day-number {
color: var(--accent);
}
/* Events in month grid */
.month-events {
display: flex;
flex-direction: column;
gap: 1px;
}
.month-event {
font-size: 11px;
padding: 1px 4px;
border-radius: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: opacity 0.1s;
color: #fff;
}
.month-event:hover { opacity: 0.85; }
.month-event.more-link {
background: transparent;
color: var(--text-dim);
font-size: 10px;
font-style: italic;
}
/* Week view */
.week-view {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
gap: 1px;
background: var(--border);
overflow-x: auto;
}
.hour-label {
background: var(--bg3);
padding: 4px;
text-align: right;
font-size: 11px;
color: var(--text-dim);
min-height: 40px;
border-bottom: 1px solid var(--border);
}
.hour-slot {
background: var(--bg2);
min-height: 40px;
border-bottom: 1px solid var(--border);
cursor: pointer;
position: relative;
}
.hour-slot:hover { background: var(--bg3); }
.week-event {
position: absolute;
left: 2px;
right: 2px;
padding: 2px 4px;
font-size: 11px;
border-radius: 3px;
cursor: pointer;
overflow: hidden;
color: #fff;
}
/* Day view */
.day-view {
padding: 12px 16px;
}
.day-events-list { display: flex; flex-direction: column; gap: 8px; }
.day-event-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 14px;
cursor: pointer;
transition: background 0.1s;
border-left: 4px solid var(--accent);
}
.day-event-card:hover { background: var(--bg3); }
.day-event-time { font-size: 12px; color: var(--text-dim); }
.day-event-title { font-weight: 600; margin-top: 2px; }
.day-event-desc { font-size: 13px; color: var(--text-dim); margin-top: 4px; }
.day-event-category { font-size: 11px; margin-top: 4px; display: inline-block; padding: 1px 6px; border-radius: 3px; }
/* Modal */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius);
max-width: 480px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
padding: 24px;
}
.modal h3 { margin-bottom: 16px; color: var(--text-bright); }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 4px; }
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 8px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-size: 14px;
}
.form-group textarea { resize: vertical; min-height: 60px; }
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
outline: none; border-color: var(--accent);
}
.form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
.btn {
padding: 8px 18px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.15s;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg3); color: var(--text); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--bg3); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { opacity: 0.85; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.checkbox-row { display: flex; align-items: center; gap: 8px; }
.checkbox-row input[type="checkbox"] { width: auto; }
/* Categories legend */
.categories-bar {
display: flex;
gap: 8px;
padding: 8px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
align-items: center;
}
.cat-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
cursor: pointer;
transition: opacity 0.15s;
color: #fff;
}
.cat-tag:hover { opacity: 0.8; }
.cat-tag.active { box-shadow: 0 0 0 2px var(--text-bright); }
.cat-filter-all {
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
padding: 2px 6px;
}
/* Toast */
.toast {
position: fixed;
bottom: 16px;
right: 16px;
background: var(--bg3);
border: 1px solid var(--border);
padding: 10px 16px;
border-radius: var(--radius-sm);
font-size: 13px;
color: var(--text-bright);
z-index: 2000;
animation: slideIn 0.25s ease;
}
@keyframes slideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Loading */
.loading-screen {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-dim);
font-size: 16px;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* Empty state */
.empty-day { color: var(--text-dim); padding: 20px; text-align: center; }
/* Responsive */
@media (max-width: 600px) {
.header { flex-direction: column; align-items: stretch; }
.view-tabs { justify-content: center; }
.day-cell { min-height: 60px; }
}
</style>
</head>
<body>
<div id="app">
<div class="loading-screen">⏳ Загрузка календаря...</div>
</div>
<script>
(function() {
'use strict';
// ─── State ────────────────────────────────────────────────────────
const state = {
view: 'month', // month | week | day
currentDate: new Date(), // the "focus" date (what month/week/day we're looking at)
selectedDate: null, // clicked day (for modal)
events: [],
categories: [],
filterCatId: null, // null = all
eventsLoaded: false,
dropDate: null, // date string being dragged onto
panelReady: false,
};
// Cache DOM refs
let appEl;
// ─── Helpers ───────────────────────────────────────────────────────
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function fmtDate(d) { return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()); }
function fmtISO(d) { return fmtDate(d) + 'T00:00:00'; }
function fmtMonth(d) { return d.getFullYear() + '-' + pad(d.getMonth()+1); }
function dayOfWeek(d) { const w = d.getDay(); return w === 0 ? 7 : w; } // Mon=1..Sun=7
function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
function parseDate(s) { const d = new Date(s); if (isNaN(d.getTime())) return null; return d; }
function shortTime(s) {
if (!s) return '';
const d = parseDate(s);
if (!d) return s;
return pad(d.getHours()) + ':' + pad(d.getMinutes());
}
function shortDate(s) {
if (!s) return '';
const d = parseDate(s);
if (!d) return s;
return pad(d.getDate()) + '.' + pad(d.getMonth()+1);
}
function fromNow(d) {
const now = new Date();
const diff = d.getTime() - now.getTime();
if (diff < 0) return 'прошло';
const mins = Math.round(diff / 60000);
if (mins < 60) return 'через ' + mins + ' мин';
const hours = Math.round(mins / 60);
if (hours < 24) return 'через ' + hours + ' ч';
const days = Math.round(hours / 24);
return 'через ' + days + ' дн';
}
// ─── Communication ─────────────────────────────────────────────────
function sendToParent(action, data) {
const msg = { source: 'calendar-plugin', action: action, data: data || {} };
if (window.parent && window.parent !== window) {
window.parent.postMessage(msg, '*');
}
}
// Request events from parent
function requestEvents() {
const start = getViewStart();
const end = getViewEnd();
sendToParent('get-events', { start: fmtISO(start), end: fmtISO(end) });
}
function getViewStart() {
const d = state.currentDate;
if (state.view === 'month') {
const first = new Date(d.getFullYear(), d.getMonth(), 1);
// Go back to Monday
const wd = dayOfWeek(first);
first.setDate(first.getDate() - (wd - 1));
return first;
}
if (state.view === 'week') {
const wd = dayOfWeek(d);
const monday = new Date(d);
monday.setDate(monday.getDate() - (wd - 1));
return monday;
}
return new Date(d);
}
function getViewEnd() {
const start = getViewStart();
const end = new Date(start);
if (state.view === 'month') end.setDate(end.getDate() + 42); // 6 weeks
else if (state.view === 'week') end.setDate(end.getDate() + 7);
else end.setDate(end.getDate() + 1);
return end;
}
// ─── Listen for parent messages ──────────────────────────────────────
window.addEventListener('message', function(e) {
const msg = e.data;
if (!msg || msg.source !== 'verstak') return;
switch (msg.type) {
case 'calendar-data':
try { window.parent.go.main.App.WriteDebugLog('[iframe] received calendar-data, events=' + (msg.events ? msg.events.length : 0) + ', categories=' + (msg.categories ? msg.categories.length : 0)); } catch(e) { try { console.log('iframe debug error: ' + e); } catch(e2) {} }
if (msg.events) state.events = msg.events;
if (msg.categories) state.categories = msg.categories;
state.eventsLoaded = true;
state.panelReady = true;
render();
break;
case 'drop':
// Dragged from another section
if (msg.date) {
state.dropDate = msg.date;
openCreateModal(msg.date, {
node_id: msg.data?.node_id || '',
link_type: msg.data?.link_type || 'node',
title: msg.data?.title || '',
});
}
break;
case 'event-created':
case 'event-updated':
case 'event-deleted':
requestEvents();
break;
}
});
// ─── Tell parent we're ready ─────────────────────────────────────────
function init() {
appEl = document.getElementById('app');
sendToParent('ready', { version: '1.0' });
// Request initial data
setTimeout(requestEvents, 100);
// Render immediately with empty state
render();
}
// ─── Render ──────────────────────────────────────────────────────────
function render() {
if (!appEl) return;
if (!state.eventsLoaded) {
appEl.innerHTML = '<div class="loading-screen">⏳ Загрузка календаря...</div>';
return;
}
appEl.innerHTML = renderHeader() + renderCategories() + renderView();
}
function renderHeader() {
const d = state.currentDate;
const monthNames = ['Январь','Февраль','Март','Апрель','Май','Июнь',
'Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
let title = '';
if (state.view === 'month') title = monthNames[d.getMonth()] + ' ' + d.getFullYear();
else if (state.view === 'week') title = 'Неделя ' + fmtDate(getViewStart());
else title = fmtDate(d);
return `<div class="header">
<div class="header-left">
<button class="nav-btn" onclick="calendar.prevPeriod()">◀</button>
<button class="nav-btn today-btn" onclick="calendar.goToday()">Сегодня</button>
<button class="nav-btn" onclick="calendar.nextPeriod()">▶</button>
<span class="header-title">${title}</span>
</div>
<div class="view-tabs">
<button class="view-tab${state.view==='month'?' active':''}" onclick="calendar.setView('month')">Месяц</button>
<button class="view-tab${state.view==='week'?' active':''}" onclick="calendar.setView('week')">Неделя</button>
<button class="view-tab${state.view==='day'?' active':''}" onclick="calendar.setView('day')">День</button>
</div>
</div>`;
}
function renderCategories() {
const cats = state.categories;
if (!cats || cats.length === 0) return '';
let html = '<div class="categories-bar">';
html += `<span class="cat-filter-all" onclick="calendar.setFilter(null)">${state.filterCatId === null ? '●' : '○'} Все</span>`;
for (const c of cats) {
const active = c.id === state.filterCatId;
html += `<span class="cat-tag${active ? ' active' : ''}" style="background:${c.color}" onclick="calendar.setFilter('${c.id}')">${c.icon || ''} ${c.name}</span>`;
}
html += '</div>';
return html;
}
function getEventsForDate(dateStr) {
const filtered = state.filterCatId
? state.events.filter(e => e.category_id === state.filterCatId)
: state.events;
return filtered.filter(e => {
const eStart = (e.start || '').substring(0, 10);
const eEnd = (e.end || e.start || '').substring(0, 10);
return dateStr >= eStart && dateStr <= eEnd;
});
}
function renderView() {
if (state.view === 'month') return renderMonth();
if (state.view === 'week') return renderWeek();
return renderDay();
}
function renderMonth() {
const d = state.currentDate;
const y = d.getFullYear(), m = d.getMonth();
const firstDay = new Date(y, m, 1);
const startDay = dayOfWeek(firstDay); // Mon=1..Sun=7
const totalDays = daysInMonth(y, m);
const todayStr = fmtDate(new Date());
const selectedStr = state.selectedDate ? fmtDate(state.selectedDate) : null;
// Previous month padding
const prevMonthDays = daysInMonth(y, m - 1);
let cells = [];
const startOffset = startDay - 1; // how many prev-month cells
// Day headers
const dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
let html = '<div class="month-grid">';
for (const n of dayNames) {
html += `<div class="day-header">${n}</div>`;
}
// Fill cells
const totalCells = Math.ceil((startOffset + totalDays) / 7) * 7;
for (let i = 0; i < totalCells; i++) {
let dayNum, cellDate, isOther = false;
if (i < startOffset) {
dayNum = prevMonthDays - startOffset + i + 1;
cellDate = new Date(y, m - 1, dayNum);
isOther = true;
} else if (i >= startOffset + totalDays) {
dayNum = i - startOffset - totalDays + 1;
cellDate = new Date(y, m + 1, dayNum);
isOther = true;
} else {
dayNum = i - startOffset + 1;
cellDate = new Date(y, m, dayNum);
}
const dateStr = fmtDate(cellDate);
const isToday = dateStr === todayStr;
const isSelected = selectedStr === dateStr;
const isDrop = state.dropDate === dateStr;
const events = getEventsForDate(dateStr);
const isWeekend = dayOfWeek(cellDate) >= 6;
let cls = 'day-cell';
if (isOther) cls += ' other-month';
if (isToday) cls += ' today';
if (isSelected) cls += ' selected';
if (isDrop) cls += ' drop-target';
html += `<div class="${cls}" onclick="calendar.selectDay('${dateStr}')" data-date="${dateStr}">`;
html += `<div class="day-number">${dayNum}</div>`;
if (events.length > 0) {
html += '<div class="month-events">';
const maxShow = 3;
for (let j = 0; j < Math.min(events.length, maxShow); j++) {
const ev = events[j];
const color = ev.color || '#6366f1';
html += `<div class="month-event" style="background:${color}" onclick="event.stopPropagation(); calendar.openEvent('${ev.id}')">${ev.title}</div>`;
}
if (events.length > maxShow) {
html += `<div class="month-event more-link" onclick="event.stopPropagation(); calendar.setView('day'); state.currentDate = new Date('${dateStr}'); render();">+${events.length - maxShow} ещё</div>`;
}
html += '</div>';
}
html += '</div>';
}
html += '</div>';
return html;
}
function renderWeek() {
const start = getViewStart();
const todayStr = fmtDate(new Date());
let html = '<div class="week-view">';
// Corner label
html += '<div class="hour-label"></div>';
for (let d = 0; d < 7; d++) {
const day = new Date(start);
day.setDate(day.getDate() + d);
const dateStr = fmtDate(day);
const dayNames = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс'];
html += `<div class="day-header" style="${dayOfWeek(day) >= 6 ? 'color:var(--danger)' : ''}">${dayNames[d]} ${day.getDate()}</div>`;
}
// Hours
for (let h = 0; h < 24; h++) {
html += `<div class="hour-label">${pad(h)}:00</div>`;
for (let d = 0; d < 7; d++) {
const day = new Date(start);
day.setDate(day.getDate() + d);
const dateStr = fmtDate(day);
const eventsOnHour = getEventsForDate(dateStr).filter(ev => {
const evH = parseInt((ev.start || '').substring(11, 13));
return evH === h;
});
let cellHtml = `<div class="hour-slot" onclick="calendar.selectDay('${dateStr}')" data-date="${dateStr}">`;
for (const ev of eventsOnHour) {
const color = ev.color || '#6366f1';
cellHtml += `<div class="week-event" style="background:${color}; top:2px" onclick="event.stopPropagation(); calendar.openEvent('${ev.id}')">${shortTime(ev.start)} ${ev.title}</div>`;
}
cellHtml += '</div>';
html += cellHtml;
}
}
html += '</div>';
return html;
}
function renderDay() {
const d = state.currentDate;
const dateStr = fmtDate(d);
const events = getEventsForDate(dateStr);
const todayStr = fmtDate(new Date());
const isToday = dateStr === todayStr;
let html = `<div class="day-view">`;
html += `<h3 style="margin-bottom:12px">${fmtDate(d)}${isToday ? ' — Сегодня' : ''}</h3>`;
html += `<button class="btn btn-primary btn-sm" onclick="calendar.selectDay('${dateStr}')" style="margin-bottom:12px">+ Добавить событие</button>`;
if (events.length === 0) {
html += '<div class="empty-day">Нет событий на этот день</div>';
} else {
html += '<div class="day-events-list">';
for (const ev of events) {
const color = ev.category_color || ev.color || '#6366f1';
const catName = ev.category_name || '';
html += `<div class="day-event-card" style="border-left-color:${color}" onclick="calendar.openEvent('${ev.id}')">`;
if (ev.all_day == 1) {
html += `<div class="day-event-time">Весь день</div>`;
} else {
html += `<div class="day-event-time">${shortTime(ev.start)}${shortTime(ev.end)}</div>`;
}
html += `<div class="day-event-title">${ev.title}</div>`;
if (ev.description) {
html += `<div class="day-event-desc">${ev.description}</div>`;
}
if (catName) {
html += `<span class="day-event-category" style="background:${color}20; color:${color}">${catName}</span>`;
}
html += `</div>`;
}
html += '</div>';
}
html += '</div>';
return html;
}
// ─── Event Modal ─────────────────────────────────────────────────────
function openCreateModal(dateStr, prefill) {
prefill = prefill || {};
const cats = state.categories;
const todayStr = fmtDate(new Date());
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.onclick = function(e) { if (e.target === modal) closeModal(); };
let catOptions = '<option value="">Без категории</option>';
for (const c of cats) {
catOptions += `<option value="${c.id}" style="color:${c.color}">${c.icon || ''} ${c.name}</option>`;
}
modal.innerHTML = `<div class="modal" onclick="event.stopPropagation()">
<h3>${prefill.node_id ? '📎 Событие из узла' : '📅 Новое событие'}</h3>
<div class="form-group">
<label>Название</label>
<input id="ev-title" value="${escapeHtml(prefill.title || '')}" placeholder="Введите название события">
</div>
<div class="form-row">
<div class="form-group">
<label>Дата начала</label>
<input id="ev-start" type="date" value="${dateStr || todayStr}">
</div>
<div class="form-group">
<label>Время</label>
<input id="ev-start-time" type="time" value="09:00">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Дата окончания</label>
<input id="ev-end" type="date" value="${dateStr || todayStr}">
</div>
<div class="form-group">
<label>Время</label>
<input id="ev-end-time" type="time" value="10:00">
</div>
</div>
<div class="checkbox-row">
<input id="ev-allday" type="checkbox">
<label for="ev-allday">Весь день</label>
</div>
<div class="form-group">
<label>Описание</label>
<textarea id="ev-desc" placeholder="Описание события (необязательно)"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Категория</label>
<select id="ev-category">${catOptions}</select>
</div>
</div>
${prefill.node_id ? `<input type="hidden" id="ev-node-id" value="${escapeHtml(prefill.node_id)}">
<input type="hidden" id="ev-link-type" value="${escapeHtml(prefill.link_type || 'node')}">` : ''}
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button class="btn btn-primary" onclick="submitCreate()">Создать</button>
</div>
</div>`;
document.body.appendChild(modal);
// Toggle time fields when "all day" is checked
document.getElementById('ev-allday').addEventListener('change', function() {
document.getElementById('ev-start-time').disabled = this.checked;
document.getElementById('ev-end-time').disabled = this.checked;
});
}
// Expose to global for onclick handlers
window.closeModal = function() {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(m => m.remove());
};
window.submitCreate = function() {
const title = document.getElementById('ev-title').value.trim();
if (!title) { showToast('Название обязательно'); return; }
const startDate = document.getElementById('ev-start').value;
const endDate = document.getElementById('ev-end').value;
const allDay = document.getElementById('ev-allday').checked;
const startTime = allDay ? '00:00' : document.getElementById('ev-start-time').value;
const endTime = allDay ? '00:00' : document.getElementById('ev-end-time').value;
const description = document.getElementById('ev-desc').value.trim();
const categoryId = document.getElementById('ev-category').value;
const nodeId = document.getElementById('ev-node-id')?.value || '';
const linkType = document.getElementById('ev-link-type')?.value || 'node';
sendToParent('create-event', {
title: title,
start: startDate + 'T' + startTime + ':00',
end: endDate + 'T' + endTime + ':00',
all_day: allDay,
description: description,
category_id: categoryId,
node_id: nodeId,
link_type: linkType,
});
closeModal();
showToast('✅ Событие создаётся...');
};
function openEditModal(eventId) {
const ev = state.events.find(e => e.id === eventId);
if (!ev) { showToast('Событие не найдено'); return; }
const cats = state.categories;
const startDate = (ev.start || '').substring(0, 10);
const startTime = (ev.start || '').substring(11, 16);
const endDate = (ev.end || ev.start || '').substring(0, 10);
const endTime = (ev.end || ev.start || '').substring(11, 16);
const allDay = ev.all_day == 1;
const completed = ev.completed == 1;
let catOptions = '<option value="">Без категории</option>';
for (const c of cats) {
catOptions += `<option value="${c.id}" ${c.id === ev.category_id ? 'selected' : ''} style="color:${c.color}">${c.icon || ''} ${c.name}</option>`;
}
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.onclick = function(e) { if (e.target === modal) closeModal(); };
modal.innerHTML = `<div class="modal" onclick="event.stopPropagation()">
<h3>✏️ Редактировать событие</h3>
<div class="form-group">
<label>Название</label>
<input id="ev-title" value="${escapeHtml(ev.title)}">
</div>
<div class="form-row">
<div class="form-group">
<label>Дата начала</label>
<input id="ev-start" type="date" value="${startDate}">
</div>
<div class="form-group">
<label>Время</label>
<input id="ev-start-time" type="time" value="${startTime}" ${allDay ? 'disabled' : ''}>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Дата окончания</label>
<input id="ev-end" type="date" value="${endDate}">
</div>
<div class="form-group">
<label>Время</label>
<input id="ev-end-time" type="time" value="${endTime}" ${allDay ? 'disabled' : ''}>
</div>
</div>
<div class="checkbox-row">
<input id="ev-allday" type="checkbox" ${allDay ? 'checked' : ''}>
<label for="ev-allday">Весь день</label>
</div>
<div class="form-group">
<label>Описание</label>
<textarea id="ev-desc">${escapeHtml(ev.description || '')}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Категория</label>
<select id="ev-category">${catOptions}</select>
</div>
</div>
<div class="checkbox-row">
<input id="ev-completed" type="checkbox" ${completed ? 'checked' : ''}>
<label for="ev-completed">Выполнено</label>
</div>
${ev.node_id ? `<div style="margin-top:8px;font-size:12px;color:var(--text-dim)">📎 Связано с узлом: ${escapeHtml(ev.node_id)}</div>` : ''}
<div class="modal-actions">
<button class="btn btn-danger" onclick="submitDelete('${ev.id}')">Удалить</button>
<button class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button class="btn btn-primary" onclick="submitUpdate('${ev.id}')">Сохранить</button>
</div>
</div>`;
document.body.appendChild(modal);
document.getElementById('ev-allday').addEventListener('change', function() {
document.getElementById('ev-start-time').disabled = this.checked;
document.getElementById('ev-end-time').disabled = this.checked;
});
}
window.submitUpdate = function(id) {
const title = document.getElementById('ev-title').value.trim();
if (!title) { showToast('Название обязательно'); return; }
const allDay = document.getElementById('ev-allday').checked;
const startDate = document.getElementById('ev-start').value;
const startTime = allDay ? '00:00' : document.getElementById('ev-start-time').value;
const endDate = document.getElementById('ev-end').value;
const endTime = allDay ? '00:00' : document.getElementById('ev-end-time').value;
const completed = document.getElementById('ev-completed')?.checked || false;
sendToParent('update-event', {
id: id,
title: title,
start: startDate + 'T' + startTime + ':00',
end: endDate + 'T' + endTime + ':00',
all_day: allDay,
description: document.getElementById('ev-desc').value.trim(),
category_id: document.getElementById('ev-category').value,
completed: completed,
});
closeModal();
showToast('✅ Сохраняю...');
};
window.submitDelete = function(id) {
if (!confirm('Удалить это событие?')) return;
sendToParent('delete-event', { id: id });
closeModal();
showToast('🗑️ Удаляю...');
};
// ─── Navigation functions ───────────────────────────────────────────
window.calendar = {
prevPeriod: function() {
const d = state.currentDate;
if (state.view === 'month') d.setMonth(d.getMonth() - 1);
else if (state.view === 'week') d.setDate(d.getDate() - 7);
else d.setDate(d.getDate() - 1);
state.currentDate = d;
requestEvents();
render();
},
nextPeriod: function() {
const d = state.currentDate;
if (state.view === 'month') d.setMonth(d.getMonth() + 1);
else if (state.view === 'week') d.setDate(d.getDate() + 7);
else d.setDate(d.getDate() + 1);
state.currentDate = d;
requestEvents();
render();
},
goToday: function() {
state.currentDate = new Date();
requestEvents();
render();
},
setView: function(view) {
state.view = view;
requestEvents();
render();
},
setFilter: function(catId) {
state.filterCatId = catId;
render();
},
selectDay: function(dateStr) {
state.selectedDate = parseDate(dateStr);
openCreateModal(dateStr, {});
},
openEvent: function(eventId) {
openEditModal(eventId);
},
};
// ─── Toast ──────────────────────────────────────────────────────────
function showToast(msg) {
const el = document.createElement('div');
el.className = 'toast';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => { el.remove(); }, 3000);
}
// ─── Escape HTML ────────────────────────────────────────────────────
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
// ─── Init ───────────────────────────────────────────────────────────
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>