feat: Migrate calendar functionality to a database
- Replaced Redis-based calendar with a database-backed solution - Implemented database models for calendars and events - Improved error handling and logging for database interactions - Added new database functions for calendar management - Updated calendar views to reflect the database changes - Enhanced event creation and deletion processes - Refined date/time handling for better consistency
This commit is contained in:
@@ -13,8 +13,7 @@
|
||||
<p class="text-muted mb-0">Manage your events and schedule</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#newEventModal">
|
||||
<button type="button" class="btn btn-primary" onclick="openEventModal()">
|
||||
<i class="bi bi-plus-circle"></i> Create Event
|
||||
</button>
|
||||
</div>
|
||||
@@ -245,6 +244,14 @@
|
||||
</div>
|
||||
<form id="newEventForm" action="/calendar/events" method="post">
|
||||
<div class="modal-body">
|
||||
<!-- Date locked info (hidden by default) -->
|
||||
<div id="dateLockInfo" class="alert alert-info" style="display: none;">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Date Selected:</strong> <span id="selectedDateDisplay"></span>
|
||||
<br>
|
||||
<small>The date is pre-selected. You can only modify the time.</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
@@ -308,7 +315,7 @@
|
||||
|
||||
<!-- Floating Action Button (FAB) for mobile -->
|
||||
<button type="button" class="d-md-none position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow"
|
||||
data-bs-toggle="modal" data-bs-target="#newEventModal"
|
||||
onclick="openEventModal()"
|
||||
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
@@ -429,56 +436,125 @@
|
||||
timeInputs.style.display = 'none';
|
||||
startTime.removeAttribute('required');
|
||||
endTime.removeAttribute('required');
|
||||
// Clear the values to prevent validation issues
|
||||
startTime.value = '';
|
||||
endTime.value = '';
|
||||
} else {
|
||||
timeInputs.style.display = 'block';
|
||||
startTime.setAttribute('required', '');
|
||||
endTime.setAttribute('required', '');
|
||||
// Set default times if empty
|
||||
if (!startTime.value || !endTime.value) {
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
startTime.value = now.toISOString().slice(0, 16);
|
||||
endTime.value = oneHourLater.toISOString().slice(0, 16);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('newEventForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
// Handle form submission (ensure only one listener)
|
||||
const eventForm = document.getElementById('newEventForm');
|
||||
if (!eventForm.hasAttribute('data-listener-added')) {
|
||||
eventForm.setAttribute('data-listener-added', 'true');
|
||||
eventForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const allDay = document.getElementById('allDayEvent').checked;
|
||||
|
||||
if (!allDay) {
|
||||
// Convert datetime-local to RFC3339 format
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
if (startTime) {
|
||||
formData.set('start_time', new Date(startTime).toISOString());
|
||||
// Prevent double submission
|
||||
if (this.hasAttribute('data-submitting')) {
|
||||
return;
|
||||
}
|
||||
if (endTime) {
|
||||
formData.set('end_time', new Date(endTime).toISOString());
|
||||
}
|
||||
} else {
|
||||
// For all-day events, set times to start and end of day
|
||||
const today = new Date();
|
||||
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 23, 59, 59);
|
||||
this.setAttribute('data-submitting', 'true');
|
||||
|
||||
formData.set('start_time', startOfDay.toISOString());
|
||||
formData.set('end_time', endOfDay.toISOString());
|
||||
}
|
||||
const formData = new FormData(this);
|
||||
const allDay = document.getElementById('allDayEvent').checked;
|
||||
|
||||
// Submit the form
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
// Ensure all_day field is always present (checkboxes don't send false values)
|
||||
formData.set('all_day', allDay ? 'true' : 'false');
|
||||
|
||||
if (!allDay) {
|
||||
// Convert datetime-local to RFC3339 format
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
if (startTime) {
|
||||
formData.set('start_time', new Date(startTime).toISOString());
|
||||
}
|
||||
if (endTime) {
|
||||
formData.set('end_time', new Date(endTime).toISOString());
|
||||
}
|
||||
} else {
|
||||
alert('Error creating event. Please try again.');
|
||||
// For all-day events, set times to start and end of day
|
||||
let selectedDate;
|
||||
|
||||
// Check if this is a date-specific event (from calendar click)
|
||||
const modal = document.getElementById('newEventModal');
|
||||
const selectedDateStr = modal.getAttribute('data-selected-date');
|
||||
|
||||
if (selectedDateStr) {
|
||||
// Parse the date string and create in local timezone to preserve the selected date
|
||||
// selectedDateStr is in format "YYYY-MM-DD"
|
||||
const dateParts = selectedDateStr.split('-');
|
||||
const year = parseInt(dateParts[0]);
|
||||
const month = parseInt(dateParts[1]) - 1; // Month is 0-based
|
||||
const day = parseInt(dateParts[2]);
|
||||
|
||||
// Create dates in local timezone at noon to avoid any date boundary issues
|
||||
// This ensures the date stays consistent regardless of timezone when converted to UTC
|
||||
const startOfDay = new Date(year, month, day, 12, 0, 0); // Noon local time
|
||||
const endOfDay = new Date(year, month, day, 12, 0, 1); // Noon + 1 second local time
|
||||
|
||||
formData.set('start_time', startOfDay.toISOString());
|
||||
formData.set('end_time', endOfDay.toISOString());
|
||||
} else {
|
||||
// Use today's date for general "Create Event" button
|
||||
const today = new Date();
|
||||
|
||||
// Create dates in local timezone at noon to avoid date boundary issues
|
||||
const startOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 0, 0);
|
||||
const endOfDay = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12, 0, 1);
|
||||
|
||||
formData.set('start_time', startOfDay.toISOString());
|
||||
formData.set('end_time', endOfDay.toISOString());
|
||||
}
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error creating event. Please try again.');
|
||||
|
||||
// Debug: Log form data
|
||||
console.log('Submitting form data:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(key, value);
|
||||
}
|
||||
|
||||
// Submit the form with correct content type
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(formData)
|
||||
}).then(response => {
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response ok:', response.ok);
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Reset submitting flag on error
|
||||
eventForm.removeAttribute('data-submitting');
|
||||
// Get the response text to see the actual error
|
||||
return response.text().then(text => {
|
||||
console.error('Server response:', text);
|
||||
alert(`Error creating event (${response.status}): ${text}`);
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
// Reset submitting flag on error
|
||||
eventForm.removeAttribute('data-submitting');
|
||||
console.error('Network error:', error);
|
||||
alert('Network error creating event. Please try again.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Delete event function
|
||||
function deleteEvent(eventId) {
|
||||
@@ -543,8 +619,19 @@
|
||||
endTime.setHours(10, 0, 0, 0);
|
||||
|
||||
// Update the modal form with the selected date
|
||||
document.getElementById('startTime').value = startTime.toISOString().slice(0, 16);
|
||||
document.getElementById('endTime').value = endTime.toISOString().slice(0, 16);
|
||||
const startTimeInput = document.getElementById('startTime');
|
||||
const endTimeInput = document.getElementById('endTime');
|
||||
|
||||
startTimeInput.value = startTime.toISOString().slice(0, 16);
|
||||
endTimeInput.value = endTime.toISOString().slice(0, 16);
|
||||
|
||||
// Restrict date changes - set min and max to the selected date
|
||||
const minDate = selectedDate.toISOString().split('T')[0] + 'T00:00';
|
||||
const maxDate = selectedDate.toISOString().split('T')[0] + 'T23:59';
|
||||
startTimeInput.min = minDate;
|
||||
startTimeInput.max = maxDate;
|
||||
endTimeInput.min = minDate;
|
||||
endTimeInput.max = maxDate;
|
||||
|
||||
// Update modal title to show the selected date
|
||||
const modalTitle = document.getElementById('newEventModalLabel');
|
||||
@@ -556,12 +643,82 @@
|
||||
});
|
||||
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create Event for ${dateStr}`;
|
||||
|
||||
// Show date lock info
|
||||
document.getElementById('dateLockInfo').style.display = 'block';
|
||||
document.getElementById('selectedDateDisplay').textContent = dateStr;
|
||||
|
||||
// Add smart time validation for date-locked events
|
||||
startTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(this.value);
|
||||
const endTime = new Date(endTimeInput.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Set end time to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Update end time minimum to be after start time
|
||||
endTimeInput.min = this.value;
|
||||
});
|
||||
|
||||
endTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(startTimeInput.value);
|
||||
const endTime = new Date(this.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Reset to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
this.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
|
||||
// Add a flag to indicate this is a date-specific event
|
||||
document.getElementById('newEventModal').setAttribute('data-date-locked', 'true');
|
||||
document.getElementById('newEventModal').setAttribute('data-selected-date', selectedDate.toISOString().split('T')[0]);
|
||||
|
||||
// Show the modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('newEventModal'));
|
||||
modal.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Open event modal for general event creation (not date-specific)
|
||||
function openEventModal() {
|
||||
// Reset modal to allow full date/time selection
|
||||
const modal = document.getElementById('newEventModal');
|
||||
const startTimeInput = document.getElementById('startTime');
|
||||
const endTimeInput = document.getElementById('endTime');
|
||||
|
||||
// Remove date restrictions
|
||||
startTimeInput.removeAttribute('min');
|
||||
startTimeInput.removeAttribute('max');
|
||||
endTimeInput.removeAttribute('min');
|
||||
endTimeInput.removeAttribute('max');
|
||||
|
||||
// Set default times to current time + 1 hour
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
|
||||
startTimeInput.value = now.toISOString().slice(0, 16);
|
||||
endTimeInput.value = oneHourLater.toISOString().slice(0, 16);
|
||||
|
||||
// Reset modal title
|
||||
const modalTitle = document.getElementById('newEventModalLabel');
|
||||
modalTitle.innerHTML = `<i class="bi bi-plus-circle"></i> Create New Event`;
|
||||
|
||||
// Hide date lock info
|
||||
document.getElementById('dateLockInfo').style.display = 'none';
|
||||
|
||||
// Remove date-locked flag
|
||||
modal.removeAttribute('data-date-locked');
|
||||
modal.removeAttribute('data-selected-date');
|
||||
|
||||
// Show the modal
|
||||
const bootstrapModal = new bootstrap.Modal(modal);
|
||||
bootstrapModal.show();
|
||||
}
|
||||
|
||||
// Initialize calendar features
|
||||
function initializeCalendar() {
|
||||
// Highlight today's date
|
||||
|
@@ -5,29 +5,29 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<h1>Create New Event</h1>
|
||||
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/calendar/new" method="post">
|
||||
|
||||
<form action="/calendar/events" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Event Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="all_day" name="all_day">
|
||||
<label class="form-check-label" for="all_day">All Day Event</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label for="start_time" class="form-label">Start Time</label>
|
||||
@@ -38,7 +38,14 @@
|
||||
<input type="datetime-local" class="form-control" id="end_time" name="end_time" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Show selected date info when coming from calendar date click -->
|
||||
<div id="selected-date-info" class="alert alert-info" style="display: none;">
|
||||
<strong>Selected Date:</strong> <span id="selected-date-display"></span>
|
||||
<br>
|
||||
<small>The date is pre-selected. You can only modify the time portion.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="color" class="form-label">Event Color</label>
|
||||
<select class="form-control" id="color" name="color">
|
||||
@@ -50,7 +57,7 @@
|
||||
<option value="#24C1E0">Cyan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">Create Event</button>
|
||||
<a href="/calendar" class="btn btn-secondary">Cancel</a>
|
||||
@@ -59,37 +66,106 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check if we came from a date click (URL parameter)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const selectedDate = urlParams.get('date');
|
||||
|
||||
if (selectedDate) {
|
||||
// Show the selected date info
|
||||
document.getElementById('selected-date-info').style.display = 'block';
|
||||
document.getElementById('selected-date-display').textContent = new Date(selectedDate).toLocaleDateString();
|
||||
|
||||
// Pre-fill the date portion and restrict date changes
|
||||
const startTimeInput = document.getElementById('start_time');
|
||||
const endTimeInput = document.getElementById('end_time');
|
||||
|
||||
// Set default times (9 AM to 10 AM on the selected date)
|
||||
const startDateTime = new Date(selectedDate + 'T09:00');
|
||||
const endDateTime = new Date(selectedDate + 'T10:00');
|
||||
|
||||
// Format for datetime-local input (YYYY-MM-DDTHH:MM)
|
||||
startTimeInput.value = startDateTime.toISOString().slice(0, 16);
|
||||
endTimeInput.value = endDateTime.toISOString().slice(0, 16);
|
||||
|
||||
// Set minimum and maximum date to the selected date to prevent changing the date
|
||||
const minDate = selectedDate + 'T00:00';
|
||||
const maxDate = selectedDate + 'T23:59';
|
||||
startTimeInput.min = minDate;
|
||||
startTimeInput.max = maxDate;
|
||||
endTimeInput.min = minDate;
|
||||
endTimeInput.max = maxDate;
|
||||
|
||||
// Add event listeners to ensure end time is after start time
|
||||
startTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(this.value);
|
||||
const endTime = new Date(endTimeInput.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Set end time to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Update end time minimum to be after start time
|
||||
endTimeInput.min = this.value;
|
||||
});
|
||||
|
||||
endTimeInput.addEventListener('change', function () {
|
||||
const startTime = new Date(startTimeInput.value);
|
||||
const endTime = new Date(this.value);
|
||||
|
||||
if (endTime <= startTime) {
|
||||
// Reset to 1 hour after start time
|
||||
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
this.value = newEndTime.toISOString().slice(0, 16);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No date selected, set default to current time
|
||||
const now = new Date();
|
||||
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
|
||||
|
||||
document.getElementById('start_time').value = now.toISOString().slice(0, 16);
|
||||
document.getElementById('end_time').value = oneHourLater.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
// Convert datetime-local inputs to RFC3339 format on form submission
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
document.querySelector('form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const startTime = document.getElementById('start_time').value;
|
||||
const endTime = document.getElementById('end_time').value;
|
||||
|
||||
|
||||
// Validate that end time is after start time
|
||||
if (new Date(endTime) <= new Date(startTime)) {
|
||||
alert('End time must be after start time');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to RFC3339 format
|
||||
const startRFC = new Date(startTime).toISOString();
|
||||
const endRFC = new Date(endTime).toISOString();
|
||||
|
||||
|
||||
// Create hidden inputs for the RFC3339 values
|
||||
const startInput = document.createElement('input');
|
||||
startInput.type = 'hidden';
|
||||
startInput.name = 'start_time';
|
||||
startInput.value = startRFC;
|
||||
|
||||
|
||||
const endInput = document.createElement('input');
|
||||
endInput.type = 'hidden';
|
||||
endInput.name = 'end_time';
|
||||
endInput.value = endRFC;
|
||||
|
||||
|
||||
// Remove the original inputs
|
||||
document.getElementById('start_time').removeAttribute('name');
|
||||
document.getElementById('end_time').removeAttribute('name');
|
||||
|
||||
|
||||
// Add the hidden inputs to the form
|
||||
this.appendChild(startInput);
|
||||
this.appendChild(endInput);
|
||||
|
||||
|
||||
// Submit the form
|
||||
this.submit();
|
||||
});
|
||||
|
Reference in New Issue
Block a user