1051 lines
32 KiB
HTML
Executable File
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">×</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 %} |