Initial commit: Schedule service for son

This commit is contained in:
vrubel 2025-12-30 12:23:42 +03:00
commit af2ea7be06
19 changed files with 2270 additions and 0 deletions

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Расписание</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div id="today-section" class="day-section">
<h1 class="day-title" id="today-title">Сегодня</h1>
<div id="today-content"></div>
</div>
<div id="tomorrow-section" class="day-section">
<h1 class="day-title" id="tomorrow-title">Завтра</h1>
<div id="tomorrow-content"></div>
</div>
</div>
<script src="/static/script.js"></script>
</body>
</html>

View file

@ -0,0 +1,123 @@
const TZ = 'Europe/London';
function formatDate(dateStr) {
const date = new Date(dateStr + 'T00:00:00');
// getDay() возвращает 0=воскресенье, 1=понедельник, ..., 6=суббота
// Конвертируем в формат где 0=понедельник
const jsDay = date.getDay();
const weekdayIndex = jsDay === 0 ? 6 : jsDay - 1; // 0=пн, 6=вс
const weekdays = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
const weekday = weekdays[weekdayIndex];
const day = date.getDate();
const month = months[date.getMonth()];
return `${weekday}, ${day} ${month}`;
}
function getTodayInLondon() {
// Получаем текущую дату в таймзоне London
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Europe/London',
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
return formatter.format(now);
}
function getTomorrowInLondon() {
const today = getTodayInLondon();
const date = new Date(today);
date.setDate(date.getDate() + 1);
return date.toISOString().split('T')[0];
}
function renderDay(container, titleEl, dateStr, items) {
titleEl.textContent = formatDate(dateStr);
const tasks = items.filter(item => item.kind === 'task');
const events = items.filter(item => item.kind === 'event');
let html = '';
if (tasks.length > 0) {
html += '<div class="tasks-list">';
tasks.forEach(task => {
html += `<div class="task-item">• ${task.title}</div>`;
});
html += '</div>';
}
if (events.length > 0) {
html += '<div class="events-list">';
events.sort((a, b) => a.start_time.localeCompare(b.start_time)).forEach(event => {
const endTime = calculateEndTime(event.start_time, event.duration_min);
html += `<div class="event-item">
<span class="event-time">${event.start_time}-${endTime}</span>
<span class="event-title">${event.title}</span>
</div>`;
});
html += '</div>';
}
if (tasks.length === 0 && events.length === 0) {
html = '<div class="empty-message">На этот день расписания нет.</div>';
}
container.innerHTML = html;
}
function calculateEndTime(startTime, durationMin) {
const [hour, minute] = startTime.split(':').map(Number);
const totalMinutes = hour * 60 + minute + durationMin;
const endHour = Math.floor(totalMinutes / 60);
const endMinute = totalMinutes % 60;
return `${String(endHour).padStart(2, '0')}:${String(endMinute).padStart(2, '0')}`;
}
async function loadSchedule() {
const today = getTodayInLondon();
const tomorrow = getTomorrowInLondon();
console.log('Loading schedule from', today, 'to', tomorrow);
try {
const response = await fetch(`/api/schedule?from_date=${today}&to_date=${tomorrow}`);
const data = await response.json();
console.log('Received items:', data.items);
const todayItems = data.items.filter(item => item.date === today);
const tomorrowItems = data.items.filter(item => item.date === tomorrow);
console.log('Today items:', todayItems);
console.log('Tomorrow items:', tomorrowItems);
renderDay(
document.getElementById('today-content'),
document.getElementById('today-title'),
today,
todayItems
);
renderDay(
document.getElementById('tomorrow-content'),
document.getElementById('tomorrow-title'),
tomorrow,
tomorrowItems
);
} catch (error) {
console.error('Error loading schedule:', error);
}
}
// Загружаем расписание при загрузке страницы
loadSchedule();
// Автообновление каждые 5 минут
setInterval(loadSchedule, 5 * 60 * 1000);

View file

@ -0,0 +1,100 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
padding: 20px;
font-size: 24px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.day-section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.day-title {
font-size: 36px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 3px solid #4CAF50;
}
.tasks-list {
margin-bottom: 30px;
}
.task-item {
font-size: 28px;
padding: 15px 0;
color: #555;
border-bottom: 1px solid #eee;
}
.task-item:last-child {
border-bottom: none;
}
.events-list {
margin-top: 20px;
}
.event-item {
font-size: 28px;
padding: 15px 0;
color: #333;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 15px;
}
.event-item:last-child {
border-bottom: none;
}
.event-time {
font-weight: 600;
color: #4CAF50;
min-width: 120px;
}
.event-title {
flex: 1;
}
.empty-message {
color: #999;
font-style: italic;
padding: 20px 0;
}
@media (max-width: 768px) {
body {
font-size: 20px;
padding: 15px;
}
.day-title {
font-size: 28px;
}
.task-item, .event-item {
font-size: 22px;
}
}