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

986 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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':
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>