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:
Mahmoud-Emad
2025-05-28 15:48:54 +03:00
parent d815d9d365
commit 58d1cde1ce
5 changed files with 972 additions and 191 deletions

View File

@@ -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

View File

@@ -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();
});