Files
Inventarsystem/Web/templates/terminplan.html
T

1051 lines
32 KiB
HTML
Executable File

<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %}
{% block title %}Terminplan - Inventarsystem{% endblock %}
{% block content %}
<div class="calendar-container">
<div class="calendar-header">
<h1>Schulstunden-Terminplan für Termine</h1>
<div class="calendar-actions">
<button id="prev-day">Vorheriger Tag</button>
<span id="current-day-display"></span>
<button id="next-day">Nächster Tag</button>
<button id="today">Heute</button>
<div class="calendar-view-switch" role="group" aria-label="Kalenderansicht wechseln">
<button type="button" class="calendar-view-btn active" data-calendar-view="timeGridDay">Tag</button>
<button type="button" class="calendar-view-btn" data-calendar-view="timeGridWeek">Woche</button>
<button type="button" class="calendar-view-btn" data-calendar-view="dayGridMonth">Monat</button>
</div>
<button id="new-booking" class="primary-button">Zur Termin-Konfiguration</button>
</div>
<div class="calendar-legend">
<span class="legend-item"><span class="legend-color current"></span> Aktuelle Termine</span>
<span class="legend-item"><span class="legend-color planned"></span> Geplante Termine</span>
<span class="legend-item"><span class="legend-color completed"></span> Abgeschlossene Termine</span>
<span class="legend-item"><span class="legend-color your-bookings"></span> Ihre Termine</span>
</div>
</div>
<div class="calendar-options">
<label class="checkbox-container">
<input type="checkbox" id="show-completed-bookings">
Abgeschlossene Termine anzeigen
</label>
</div>
<div class="calendar-stage">
<div id="calendar-skeleton" class="calendar-skeleton" aria-hidden="true">
<div class="skeleton-toolbar">
<span class="skeleton-pill"></span>
<span class="skeleton-pill"></span>
<span class="skeleton-pill"></span>
</div>
<div class="skeleton-grid">
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-day-header"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
<div class="skeleton-slot"></div>
</div>
</div>
<div id="calendar"></div>
</div>
<!-- Modal for event details -->
<div id="event-modal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Termin-Details</h2>
<div id="event-details">
<!-- Event details will be loaded dynamically -->
</div>
<div id="event-actions">
<button id="cancel-booking" class="danger-button">Termin stornieren</button>
</div>
</div>
</div>
</div>
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/daygrid/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/timegrid/main.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/locales/de.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize variables
const calendarEl = document.getElementById('calendar');
const eventModal = document.getElementById('event-modal');
const newBookingBtn = document.getElementById('new-booking');
let currentEventId = null;
let calendar;
let showCompletedBookings = false;
let activeCalendarView = 'timeGridDay';
// School period times from server configuration
let schoolPeriods;
try {
schoolPeriods = JSON.parse('{{ school_periods|tojson|safe }}');
} catch (e) {
// Fallback to default periods if parsing fails
schoolPeriods = {
"1": { "start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)" },
"2": { "start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)" },
"3": { "start": "09:45", "end": "10:30", "label": "3. Stunde (09:45 - 10:30)" },
"4": { "start": "10:30", "end": "11:15", "label": "4. Stunde (10:30 - 11:15)" },
"5": { "start": "11:30", "end": "12:15", "label": "5. Stunde (11:30 - 12:15)" },
"6": { "start": "12:15", "end": "13:00", "label": "6. Stunde (12:15 - 13:00)" },
"7": { "start": "13:30", "end": "14:15", "label": "7. Stunde (13:30 - 14:15)" },
"8": { "start": "14:15", "end": "15:00", "label": "8. Stunde (14:15 - 15:00)" },
"9": { "start": "15:15", "end": "16:00", "label": "9. Stunde (15:15 - 16:00)" },
"10": { "start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)" }
};
}
// Convert period to start/end times
function getPeriodTimes(date, period) {
const periodInfo = schoolPeriods[period];
if (!periodInfo) return null;
// Create a new date object with just the date component
const newDate = new Date(date);
newDate.setHours(0, 0, 0, 0); // Reset time to midnight
// Extract hours and minutes from period definition
const [startHours, startMinutes] = periodInfo.start.split(':').map(Number);
const [endHours, endMinutes] = periodInfo.end.split(':').map(Number);
// Create dates in the browser's local time zone with exact hours
const startDate = new Date(newDate);
startDate.setHours(startHours, startMinutes, 0, 0);
const endDate = new Date(newDate);
endDate.setHours(endHours, endMinutes, 0, 0);
// Add timezone offset information for server-side handling
const tzOffset = startDate.getTimezoneOffset();
return {
start: startDate,
end: endDate,
timezoneOffset: tzOffset
};
}
// Get period from a time
function getPeriodFromTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const timeStr = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
for (const [period, times] of Object.entries(schoolPeriods)) {
if (timeStr >= times.start && timeStr <= times.end) {
return parseInt(period);
}
}
return null;
}
// Initialize calendar
calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'timeGridDay',
locale: 'de',
headerToolbar: false, // We're using our custom header
navLinks: true,
editable: false,
dayMaxEvents: true,
allDaySlot: false,
slotMinTime: '08:00:00',
slotMaxTime: '16:45:00',
slotDuration: '00:45:00',
snapDuration: '00:45:00',
scrollTime: '08:00:00',
nowIndicator: true,
now: new Date(),
timeZone: 'local',
displayEventTime: true,
height: 'auto',
contentHeight: 'auto',
handleWindowResize: true,
eventDisplay: 'block',
forceEventDuration: true,
eventTimeFormat: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
loading: function(isLoading) {
const skeleton = document.getElementById('calendar-skeleton');
if (skeleton) {
skeleton.classList.toggle('is-hidden', !isLoading);
}
},
selectAllow: function(info) {
const startHour = info.start.getHours();
const endHour = info.end.getHours();
return startHour >= 8 && endHour <= 17;
},
events: function(info, successCallback, failureCallback) {
const startStr = info.startStr;
const endStr = info.endStr;
// Load events from the server
fetch('/get_user_appointments?start=' + startStr + '&end=' + endStr)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Extract bookings from response
const bookingsArray = Array.isArray(data) ? data : (data.bookings || []);
// Process events to add colors based on status
let processedEvents = bookingsArray.map(event => {
let eventColor = '#3788d8'; // Default color
if (event.isCurrentUser) {
eventColor = '#8e44ad'; // Purple for current user's bookings
} else if (event.status === 'completed') {
eventColor = '#7f8c8d'; // Gray for completed bookings
} else if (event.status === 'current') {
eventColor = '#e74c3c'; // Red for current bookings
} else if (event.status === 'planned') {
eventColor = '#2ecc71'; // Green for planned bookings
}
// Ensure we have valid dates for start and end
let startTime, endTime;
try {
startTime = new Date(event.start);
endTime = new Date(event.end);
// Verify dates are valid
if (isNaN(startTime) || isNaN(endTime)) {
const now = new Date();
startTime = now;
endTime = new Date(now.getTime() + 45*60000); // 45 minutes later
}
} catch (e) {
const now = new Date();
startTime = now;
endTime = new Date(now.getTime() + 45*60000);
}
// Create the processed event object
return {
id: event.id,
title: event.title || "Unnamed Event",
start: startTime.toISOString(),
end: endTime.toISOString(),
allDay: false,
extendedProps: {
itemId: event.itemId,
userName: event.userName || "Unknown User",
notes: event.notes || "",
status: event.status || "unknown",
isCurrentUser: Boolean(event.isCurrentUser),
period: event.period
},
backgroundColor: eventColor,
borderColor: eventColor,
textColor: '#ffffff',
display: 'block',
classNames: [
(event.status || "unknown") + '-event',
event.isCurrentUser ? 'user-event' : '',
'custom-event'
]
};
});
// Filter completed if needed
const filteredEvents = showCompletedBookings
? processedEvents
: processedEvents.filter(event => event.extendedProps.status !== 'completed');
// Provide events to calendar
successCallback(filteredEvents);
})
.catch(error => {
failureCallback(error);
});
},
eventDidMount: function(info) {
// Add custom attributes for styling
if (info.event.extendedProps.isCurrentUser) {
info.el.setAttribute('data-is-current-user', 'true');
info.el.classList.add('user-event-visible');
}
// Add visible period marker if available
if (info.event.extendedProps.period) {
const periodDiv = document.createElement('div');
periodDiv.className = 'fc-event-period-marker';
periodDiv.textContent = info.event.extendedProps.period + '. Std';
periodDiv.style.backgroundColor = 'rgba(255,255,255,0.3)';
periodDiv.style.borderRadius = '3px';
periodDiv.style.padding = '1px 3px';
periodDiv.style.marginTop = '2px';
periodDiv.style.display = 'inline-block';
periodDiv.style.fontSize = '10px';
info.el.appendChild(periodDiv);
}
// Add borrowed indicator if relevant
if (info.event.extendedProps.itemBorrower &&
info.event.extendedProps.itemBorrower !== info.event.extendedProps.userName) {
const borrowerIndicator = document.createElement('div');
borrowerIndicator.className = 'borrowed-indicator';
borrowerIndicator.innerHTML = '⚠️'; // Warning symbol
borrowerIndicator.title = `Bereits verknüpft mit: ${info.event.extendedProps.itemBorrower}`;
borrowerIndicator.style.position = 'absolute';
borrowerIndicator.style.top = '2px';
borrowerIndicator.style.left = '2px';
borrowerIndicator.style.zIndex = '102';
borrowerIndicator.style.fontSize = '12px';
info.el.appendChild(borrowerIndicator);
info.el.classList.add('already-borrowed');
}
},
eventClick: function(info) {
showEventDetails(info.event);
},
datesSet: function() {
updateCurrentDayDisplay();
}
});
function setCalendarView(viewName) {
activeCalendarView = viewName;
calendar.changeView(viewName);
document.querySelectorAll('[data-calendar-view]').forEach(button => {
button.classList.toggle('active', button.dataset.calendarView === viewName);
});
}
document.querySelectorAll('[data-calendar-view]').forEach(button => {
button.addEventListener('click', function() {
setCalendarView(this.dataset.calendarView);
});
});
// Render calendar
try {
const calendarSkeleton = document.getElementById('calendar-skeleton');
if (calendarSkeleton) {
calendarSkeleton.classList.remove('is-hidden');
}
calendar.render();
setCalendarView(activeCalendarView);
setTimeout(() => {
calendar.updateSize();
calendar.refetchEvents();
}, 500);
} catch (e) {
console.error("Error rendering calendar:", e);
}
updateCurrentDayDisplay();
// Custom navigation buttons
document.getElementById('prev-day').addEventListener('click', function() {
calendar.prev();
updateCurrentDayDisplay();
});
document.getElementById('next-day').addEventListener('click', function() {
calendar.next();
updateCurrentDayDisplay();
});
document.getElementById('today').addEventListener('click', function() {
calendar.today();
updateCurrentDayDisplay();
});
// Update the current day display
function updateCurrentDayDisplay() {
const dateStr = calendar.view.title;
document.getElementById('current-day-display').textContent = dateStr;
}
// Show event details
function showEventDetails(event) {
const details = document.getElementById('event-details');
const actions = document.getElementById('event-actions');
// Set current event ID
currentEventId = event.id;
// Get status text based on status value
let statusText = 'Unbekannt';
if (event.extendedProps.status === 'current') {
statusText = 'Aktiv';
} else if (event.extendedProps.status === 'planned') {
statusText = 'Geplant';
} else if (event.extendedProps.status === 'completed') {
statusText = 'Abgeschlossen';
}
// Get period information if available
let periodText = '';
if (event.extendedProps.period) {
const period = event.extendedProps.period;
if (schoolPeriods[period]) {
periodText = `<p><strong>Schulstunde:</strong> ${schoolPeriods[period].label}</p>`;
}
}
// Check if item is already borrowed by someone else
let borrowerInfo = '';
if (event.extendedProps.itemBorrower && event.extendedProps.itemBorrower !== event.extendedProps.userName) {
borrowerInfo = `<p class="warning"><strong>Hinweis:</strong> Dieser Termin ist aktuell bereits mit <strong>${event.extendedProps.itemBorrower}</strong> verknüpft.</p>`;
}
// Populate details
details.innerHTML = `
<p><strong>Termin:</strong> ${event.title}</p>
<p><strong>Datum:</strong> ${new Date(event.start).toLocaleDateString('de-DE')}</p>
<p><strong>Von:</strong> ${new Date(event.start).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
<p><strong>Bis:</strong> ${new Date(event.end).toLocaleTimeString('de-DE', {hour: '2-digit', minute:'2-digit'})}</p>
${periodText}
<p><strong>Erstellt von:</strong> ${event.extendedProps.userName}</p>
${event.extendedProps.notes ? `<p><strong>Notizen:</strong> ${event.extendedProps.notes}</p>` : ''}
<p><strong>Status:</strong> ${statusText}</p>
${borrowerInfo}
`;
// Show/hide cancel button based on ownership and status
// Only allow cancellation for planned bookings that belong to the current user
if (event.extendedProps.isCurrentUser && event.extendedProps.status === 'planned') {
actions.style.display = 'block';
} else {
actions.style.display = 'none';
}
// Show modal
eventModal.style.display = 'block';
}
// Route users to the dedicated configuration flow instead of creating bookings here.
newBookingBtn.addEventListener('click', function() {
window.location.href = '/terminplaner/configure';
});
// Close event details modal when clicking X
document.querySelectorAll('#event-modal .close').forEach(closeBtn => {
closeBtn.addEventListener('click', function() {
eventModal.style.display = 'none';
});
});
// Close event details modal when clicking outside
window.addEventListener('click', function(event) {
if (event.target === eventModal) {
eventModal.style.display = 'none';
}
});
// Cancel booking
document.getElementById('cancel-booking').addEventListener('click', function() {
if (confirm('Möchten Sie diesen Termin wirklich stornieren?')) {
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
fetch('/cancel_booking/' + currentEventId, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
eventModal.style.display = 'none';
calendar.refetchEvents();
alert('Termin erfolgreich storniert!');
} else {
alert('Fehler: ' + data.error);
}
})
.catch(error => {
console.error('Error canceling booking:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
});
}
});
document.getElementById('show-completed-bookings').addEventListener('change', function() {
showCompletedBookings = this.checked;
calendar.refetchEvents();
});
});
</script>
<style>
.calendar-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.calendar-stage {
position: relative;
min-height: 620px;
}
.calendar-skeleton {
position: absolute;
inset: 0;
z-index: 5;
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 20px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 252, 0.96));
padding: 20px;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.calendar-skeleton.is-hidden {
display: none;
}
.skeleton-toolbar {
display: flex;
gap: 10px;
margin-bottom: 18px;
}
.skeleton-pill,
.skeleton-day-header,
.skeleton-slot {
position: relative;
overflow: hidden;
background: linear-gradient(90deg, #e9eef5 0%, #f7f9fc 50%, #e9eef5 100%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.skeleton-pill {
width: 90px;
height: 34px;
border-radius: 999px;
}
.skeleton-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 10px;
}
.skeleton-day-header {
height: 28px;
border-radius: 10px;
}
.skeleton-slot {
height: 72px;
border-radius: 14px;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.calendar-header {
margin-bottom: 20px;
}
.calendar-actions {
display: flex;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.calendar-actions button {
background-color: #f0f0f0;
border: 1px solid #ddd;
padding: 5px 10px;
margin: 0;
cursor: pointer;
white-space: nowrap;
}
.calendar-view-switch {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px;
border: 1px solid #d7dce3;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
}
.calendar-view-btn {
border: 0;
border-radius: 999px;
background: transparent;
color: var(--ui-text);
font-weight: 600;
padding: 7px 14px;
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
}
.calendar-view-btn.active {
background: var(--module-accent-color);
color: #fff;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12);
}
.calendar-actions .primary-button {
background-color: #3788d8;
color: white;
margin-left: auto;
}
#current-day-display {
font-weight: bold;
margin: 0 15px;
min-width: 150px;
text-align: center;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.calendar-actions {
justify-content: center;
flex-direction: column;
margin-bottom: 15px;
}
.calendar-actions button {
width: 100%;
margin: 5px 0;
}
.calendar-view-switch {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
}
.calendar-view-btn {
flex: 1 1 30%;
min-width: 88px;
}
.calendar-actions .primary-button {
margin-left: 0;
order: -1;
}
#current-day-display {
margin: 10px 0;
text-align: center;
}
.calendar-legend {
flex-wrap: wrap;
justify-content: center;
}
.legend-item {
margin-bottom: 8px;
}
}
.calendar-legend {
display: flex;
margin-top: 10px;
}
.legend-item {
display: flex;
align-items: center;
margin-right: 20px;
}
.legend-color {
display: inline-block;
width: 15px;
height: 15px;
margin-right: 5px;
border-radius: 50%;
}
.legend-color.current {
background-color: #e74c3c;
}
.legend-color.planned {
background-color: #2ecc71;
}
.legend-color.completed {
background-color: #7f8c8d;
}
.legend-color.your-bookings {
background-color: #8e44ad;
}
.calendar-options {
margin: 10px 0;
}
.checkbox-container {
display: inline-flex;
align-items: center;
cursor: pointer;
}
.checkbox-container input {
margin-right: 8px;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
border-radius: 5px;
max-width: 500px;
position: relative;
}
.close {
position: absolute;
right: 20px;
top: 10px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.primary-button {
background-color: #3788d8;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.danger-button {
background-color: #e74c3c;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
/* Make the day view time slots more readable */
.fc-timegrid-slot {
height: 3em !important;
position: relative !important;
z-index: 1 !important;
}
/* Ensure calendar container is properly sized */
#calendar {
min-height: 600px;
width: 100%;
margin-bottom: 30px;
position: relative;
z-index: 1;
}
/* Highlight the periods */
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="08:00:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="08:45:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="09:45:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="10:30:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="11:30:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="12:15:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="13:30:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="14:15:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="15:15:00"],
.fc-timegrid-slot.fc-timegrid-slot-lane[data-time="16:00:00"] {
border-top: 2px solid #3788d8;
}
/* Now indicator styles */
.fc .fc-timegrid-now-indicator-line {
border-color: #3788d8 !important;
border-width: 2px !important;
z-index: 999 !important;
}
.fc .fc-timegrid-now-indicator-arrow {
border-color: #3788d8 !important;
border-width: 5px !important;
}
.error { color: #e74c3c; }
.warning { color: #f39c12; }
.success { color: #2ecc71; }
/* Make booked periods more visible */
.fc-event {
border-width: 2px !important;
font-weight: bold !important;
padding: 2px 4px !important;
min-height: 24px !important;
position: relative !important;
z-index: 5 !important;
margin: 1px 0 !important;
opacity: 1 !important;
}
/* Add shadow to the current user's bookings for better visibility */
.fc-event[data-is-current-user="true"] {
box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important;
z-index: 10 !important; /* Make sure user's events appear on top */
}
/* Period marker within the event */
.fc-event-period-marker {
font-size: 10px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 3px;
padding: 1px 3px;
margin-top: 2px;
display: inline-block;
}
/* Add custom colors for event types to make them more distinct */
.fc-event.planned-event {
background-color: #2ecc71 !important;
border-color: #27ae60 !important;
}
.fc-event.current-event {
background-color: #e74c3c !important;
border-color: #c0392b !important;
}
.fc-event.completed-event {
background-color: #7f8c8d !important;
border-color: #34495e !important;
opacity: 0.8;
}
.fc-event.user-event {
background-color: #8e44ad !important;
border-color: #9b59b6 !important;
}
/* Make time slots for school periods more recognizable */
.fc-timegrid-slot:nth-child(odd) {
background-color: rgba(240, 240, 245, 0.4);
}
/* Class for events with special times */
.has-early-start {
border-top-width: 3px !important;
border-top-style: dashed !important;
}
.has-late-end {
border-bottom-width: 3px !important;
border-bottom-style: dashed !important;
}
/* Enhanced visibility for events */
.fc-event,
.fc-timegrid-event,
.fc-event-main,
.fc-timegrid-event-harness,
.custom-event {
border-width: 2px !important;
font-weight: bold !important;
padding: 2px 4px !important;
min-height: 24px !important;
position: absolute !important;
z-index: 999 !important;
margin: 1px 0 !important;
opacity: 1 !important;
visibility: visible !important;
display: block !important;
pointer-events: auto !important;
overflow: visible !important;
}
/* Extra specificity for user events to ensure visibility */
.user-event-visible,
.fc-event[data-is-current-user="true"],
.fc-timegrid-event[data-is-current-user="true"] {
box-shadow: 0 2px 8px rgba(0,0,0,0.5) !important;
z-index: 100 !important;
opacity: 1 !important;
border-width: 3px !important;
padding: 3px 5px !important;
}
/* Make sure event containers are visible */
.fc-timegrid-event-harness {
z-index: 50 !important;
position: absolute !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Make individual events in the grid visible */
.planned-event, .current-event, .completed-event, .user-event {
opacity: 1 !important;
visibility: visible !important;
display: block !important;
}
/* Add these styles to your existing CSS */
.weekday-selector {
display: flex;
gap: 15px;
margin-top: 5px;
flex-wrap: wrap;
}
.weekday-selector label {
display: flex;
align-items: center;
cursor: pointer;
}
.weekday-selector input {
margin-right: 5px;
}
.booking-summary {
margin: 15px 0;
padding: 10px;
background-color: var(--ui-surface-soft);
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.booking-summary h4 {
margin-top: 0;
margin-bottom: 8px;
color: #3788d8;
}
.booking-summary ul {
margin: 0;
padding-left: 20px;
}
.booking-summary li {
margin-bottom: 4px;
}
.progress-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
}
.progress-content {
background-color: white;
padding: 20px;
border-radius: 8px;
width: 80%;
max-width: 400px;
text-align: center;
}
.progress-bar-container {
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
margin: 15px 0;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #3788d8;
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
font-size: 14px;
color: #666;
}
</style>
{% endblock %}