This commit is contained in:
Tomas Dvorak
2025-06-13 15:07:46 +02:00
parent 289eb93c26
commit 8a9de3c1cb
+374 -30
View File
@@ -211,6 +211,103 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
/* Enhanced calendar event styles */
.fc-event {
border: none !important;
border-radius: 6px !important;
padding: 4px 6px !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
margin: 2px 0 !important;
background: white !important;
}
.fc-event-title {
font-weight: 500 !important;
font-size: 0.9rem !important;
}
.fc-event-time {
font-weight: 600 !important;
opacity: 0.8 !important;
}
/* Vehicle-specific event colors */
.event-vw-caddy {
border-left: 4px solid #1a73e8 !important;
background: #e6f3ff !important;
}
.event-vw-golf {
border-left: 4px solid #28a745 !important;
background: #e6ffe6 !important;
}
.event-skoda-fabia {
border-left: 4px solid #fd7e14 !important;
background: #fff3e6 !important;
}
.event-bmw-218d {
border-left: 4px solid #6f42c1 !important;
background: #f3e6ff !important;
}
.event-skoda-superb {
border-left: 4px solid #dc3545 !important;
background: #ffe6e6 !important;
}
/* Reservation list styles */
.reservations-list {
margin-top: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
overflow: hidden;
}
.reservations-list-header {
background: #f8fafc;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.reservations-list-body {
max-height: 400px;
overflow-y: auto;
}
.reservation-item {
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
gap: 1rem;
}
.reservation-item:last-child {
border-bottom: none;
}
.reservation-vehicle-badge {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
}
/* High traffic warning styles */
.high-traffic-warning {
display: none;
background: #fff3e6;
border-left: 4px solid #fd7e14;
padding: 0.75rem 1rem;
margin-top: 0.5rem;
border-radius: 0.375rem;
font-size: 0.875rem;
color: #c05621;
}
/* Form container styles */ /* Form container styles */
.form-container { .form-container {
max-width: 600px; max-width: 600px;
@@ -452,6 +549,44 @@
.reservation-modal .close-button:hover { .reservation-modal .close-button:hover {
color: #333; color: #333;
} }
/* Availability and warning styles */
.form-group button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#availabilityStatus, #highTrafficWarning {
display: flex;
align-items: center;
font-size: 0.875rem;
margin-top: 0.5rem;
padding: 0.5rem;
border-radius: 0.375rem;
transition: all 0.3s ease;
}
#availabilityStatus i, #highTrafficWarning i {
margin-right: 0.5rem;
}
#availabilityStatus.bg-green-50 {
background-color: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
#availabilityStatus.bg-red-50 {
background-color: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
#highTrafficWarning {
background-color: #fff7ed;
color: #9a3412;
border: 1px solid #fed7aa;
}
</style> </style>
</head> </head>
<body class="bg-brand-gray min-h-screen"> <body class="bg-brand-gray min-h-screen">
@@ -517,15 +652,24 @@
</div> </div>
</div> </div>
</div> </div>
<div id='calendar'></div> <div id='calendar'></div> <div class="flex justify-center mt-6">
<div class="flex justify-center mt-6">
<button id="newReservationBtn" <button id="newReservationBtn"
class="bg-brand-blue hover:bg-brand-light-blue text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200 transform hover:scale-105 flex items-center gap-2"> class="bg-brand-blue hover:bg-brand-light-blue text-white font-bold py-3 px-6 rounded-lg shadow-lg transition-all duration-200 transform hover:scale-105 flex items-center gap-2">
<i class="fas fa-plus-circle"></i> <i class="fas fa-plus-circle"></i>
Vytvořit novou rezervaci Vytvořit novou rezervaci
</button> </button>
</div> </div>
</div> <!-- Reservation Form Modal -->
<!-- Reservations List -->
<div class="reservations-list mt-8">
<div class="reservations-list-header">
<h3 class="text-lg font-semibold text-gray-800">Nadcházející rezervace</h3>
</div>
<div class="reservations-list-body" id="reservationsList">
<!-- Reservations will be populated here -->
</div>
</div>
</div><!-- Reservation Form Modal -->
<div id="reservationModal" class="reservation-modal"> <div id="reservationModal" class="reservation-modal">
<div class="form-container"> <div class="form-container">
<button type="button" class="close-button" id="closeReservationModal">&times;</button> <button type="button" class="close-button" id="closeReservationModal">&times;</button>
@@ -538,7 +682,7 @@
class="w-full p-2 border border-gray-300 rounded-md"> class="w-full p-2 border border-gray-300 rounded-md">
</div> </div>
<!-- Vehicle Selection --> <!-- Vehicle Selection with Warning -->
<div class="form-group"> <div class="form-group">
<label for="vehicle">Vozidlo</label> <label for="vehicle">Vozidlo</label>
<div class="select-wrapper"> <div class="select-wrapper">
@@ -552,6 +696,13 @@
<option value="Škoda Superb">Škoda Superb</option> <option value="Škoda Superb">Škoda Superb</option>
</select> </select>
</div> </div>
<!-- High Traffic Warning -->
<div id="highTrafficWarning" class="hidden mt-2 text-sm bg-amber-50 text-amber-700 p-2 rounded-md border border-amber-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
<span class="warning-message"></span>
</div>
<!-- Availability Status -->
<div id="availabilityStatus" class="hidden mt-2 text-sm rounded-md p-2"></div>
</div> </div>
<!-- Date and Time Selection --> <!-- Date and Time Selection -->
@@ -695,16 +846,100 @@
const reservationForm = document.getElementById('reservationForm'); const reservationForm = document.getElementById('reservationForm');
const statusMessage = document.getElementById('statusMessage'); const statusMessage = document.getElementById('statusMessage');
const modal = document.getElementById('eventModal'); const modal = document.getElementById('eventModal');
const highTrafficWarning = document.getElementById('highTrafficWarning');
const availabilityStatus = document.getElementById('availabilityStatus');
let currentEventId = null; let currentEventId = null;
// Initialize FullCalendar // Function to check availability and traffic
const calendarEl = document.getElementById('calendar'); calendar = new FullCalendar.Calendar(calendarEl, { async function checkAvailabilityAndTraffic() {
const vehicle = document.getElementById('vehicle').value;
const startDate = document.getElementById('startDate').value;
const startTime = document.getElementById('startTime').value;
const endDate = document.getElementById('endDate').value;
const endTime = document.getElementById('endTime').value;
if (!vehicle || !startDate || !startTime || !endDate || !endTime) {
return;
}
try {
const response = await fetch(`/api/check-availability?vehicle=${encodeURIComponent(vehicle)}&startDate=${startDate}&startTime=${startTime}&endDate=${endDate}&endTime=${endTime}`);
const data = await response.json();
// Update availability status
availabilityStatus.classList.remove('hidden', 'bg-green-50', 'text-green-700', 'bg-red-50', 'text-red-700');
if (data.available) {
availabilityStatus.classList.add('bg-green-50', 'text-green-700');
availabilityStatus.innerHTML = '<i class="fas fa-check-circle mr-2"></i>Vozidlo je v tomto čase k dispozici';
} else {
availabilityStatus.classList.add('bg-red-50', 'text-red-700');
availabilityStatus.innerHTML = '<i class="fas fa-times-circle mr-2"></i>Vozidlo je v tomto čase již rezervované';
}
availabilityStatus.classList.remove('hidden');
// Check for high traffic
if (data.reservationCount >= 3) {
highTrafficWarning.querySelector('.warning-message').textContent =
`Toto vozidlo má v daný den již ${data.reservationCount} rezervací.`;
highTrafficWarning.classList.remove('hidden');
} else {
highTrafficWarning.classList.add('hidden');
}
// Disable submit button if not available
const submitButton = reservationForm.querySelector('button[type="submit"]');
submitButton.disabled = !data.available;
submitButton.classList.toggle('opacity-50', !data.available);
submitButton.classList.toggle('cursor-not-allowed', !data.available);
return data.available;
} catch (error) {
console.error('Error checking availability:', error);
return false;
}
}
// Add event listeners for form inputs
const formInputs = ['vehicle', 'startDate', 'startTime', 'endDate', 'endTime'];
formInputs.forEach(inputId => {
document.getElementById(inputId).addEventListener('change', checkAvailabilityAndTraffic);
});
// Modify form submission to check availability first
reservationForm.addEventListener('submit', async function(e) {
e.preventDefault();
const isAvailable = await checkAvailabilityAndTraffic();
if (!isAvailable) {
return;
}
// Rest of the form submission code
// ...existing code...
}); // Initialize calendar
const calendarEl = document.getElementById('calendar');
function createEventContent(arg) {
return {
html: `
<div class="fc-event-main">
<div class="fc-event-time">${arg.timeText}</div>
<div class="fc-event-title">${arg.event.extendedProps.driverName}</div>
<div class="fc-event-desc text-sm">${arg.event.extendedProps.vehicle}</div>
</div>
`
};
}
const calendarConfig = {
initialView: 'timeGridWeek', initialView: 'timeGridWeek',
headerToolbar: { headerToolbar: {
left: 'prev,next today', left: 'prev,next today',
center: 'title', center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay' right: 'dayGridMonth,timeGridWeek,timeGridDay'
}, },
validRange: {
start: new Date().toISOString().split('T')[0]
},
locale: 'cs', locale: 'cs',
slotMinTime: '06:00:00', slotMinTime: '06:00:00',
slotMaxTime: '22:00:00', slotMaxTime: '22:00:00',
@@ -716,41 +951,37 @@
eventTimeFormat: { eventTimeFormat: {
hour12: false hour12: false
}, },
eventClassNames: function(arg) {
return ['event-' + arg.event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-')];
},
eventContent: {
html: function(arg) {
return `
<div class="fc-event-main">
<div class="fc-event-time">${arg.timeText}</div>
<div class="fc-event-title">${arg.event.extendedProps.driverName}</div>
<div class="fc-event-desc text-sm">${arg.event.extendedProps.vehicle}</div>
</div>
`;
}
},
dateClick: function(info) { dateClick: function(info) {
// Store the timestamp of the last click
const now = Date.now(); const now = Date.now();
if (this.lastClick && (now - this.lastClick < 300)) { if (this.lastClick && (now - this.lastClick < 300)) {
// Double click detected
showReservationForm(info.date); showReservationForm(info.date);
} }
this.lastClick = now; this.lastClick = now;
}, },
eventContent: function(arg) {
return {
html: `
<div class="p-1">
<div class="font-semibold text-sm">${arg.event.extendedProps.vehicle}</div>
<div class="text-xs">${arg.event.extendedProps.driverName}</div>
${arg.event.extendedProps.purpose ? `<div class="text-xs text-gray-600">${arg.event.extendedProps.purpose}</div>` : ''}
</div>
`
};
},
eventClick: function(info) { eventClick: function(info) {
showEventModal(info.event); showEventModal(info.event);
},
eventConstraint: {
dows: [0,1,2,3,4,5,6]
},
selectConstraint: {
startTime: '06:00',
endTime: '22:00',
dows: [0,1,2,3,4,5,6]
} }
}); };
calendar = new FullCalendar.Calendar(calendarEl, calendarConfig);
calendar.render(); calendar.render();
// Rest of the JavaScript code
// ...existing code...
// Reservation modal functions // Reservation modal functions
const reservationModal = document.getElementById('reservationModal'); const reservationModal = document.getElementById('reservationModal');
const closeReservationModal = document.getElementById('closeReservationModal'); function showReservationForm(date) { const closeReservationModal = document.getElementById('closeReservationModal'); function showReservationForm(date) {
@@ -789,6 +1020,111 @@
} }
} }
// Function to check vehicle availability and high traffic
async function checkVehicleAvailability(vehicle, startDate, endDate) {
try {
const response = await fetch(`/api/check-availability?vehicle=${encodeURIComponent(vehicle)}&startDate=${startDate}&startTime=${startTime}&endDate=${endDate}&endTime=${endTime}`);
const data = await response.json();
// Check for high traffic (more than 3 reservations for the same vehicle on the same day)
if (data.reservationCount > 3) {
showHighTrafficWarning(vehicle);
} else {
hideHighTrafficWarning();
}
return data.available;
} catch (error) {
console.error('Error checking availability:', error);
return false;
}
}
// Function to show high traffic warning
function showHighTrafficWarning(vehicle) {
const warningEl = document.querySelector('.high-traffic-warning');
if (!warningEl) {
const warning = document.createElement('div');
warning.className = 'high-traffic-warning';
warning.innerHTML = `
<div class="flex items-center gap-2">
<i class="fas fa-exclamation-triangle"></i>
<span>Upozornění: Vozidlo ${vehicle} má v tento den vysoký počet rezervací.</span>
</div>
`;
document.getElementById('vehicle').parentNode.appendChild(warning);
}
warningEl.style.display = 'block';
}
// Function to hide high traffic warning
function hideHighTrafficWarning() {
const warningEl = document.querySelector('.high-traffic-warning');
if (warningEl) {
warningEl.style.display = 'none';
}
}
// Function to update reservations list
function updateReservationsList() {
const reservationsList = document.getElementById('reservationsList');
const events = calendar.getEvents();
// Sort events by start date
events.sort((a, b) => a.start - b.start);
// Filter future events
const futureEvents = events.filter(event => event.start >= new Date());
if (futureEvents.length === 0) {
reservationsList.innerHTML = '<div class="p-4 text-gray-500">Žádné nadcházející rezervace</div>';
return;
}
reservationsList.innerHTML = futureEvents.map(event => {
const vehicleClass = 'event-' + event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-');
return `
<div class="reservation-item">
<div class="reservation-vehicle-badge ${vehicleClass}">
${event.extendedProps.vehicle}
</div>
<div class="flex-1">
<div class="font-medium">${event.extendedProps.driverName}</div>
<div class="text-sm text-gray-600">
${formatDateTime(event.start)} - ${formatDateTime(event.end)}
</div>
</div>
<div class="text-sm text-gray-500">
${event.extendedProps.purpose || 'Bez účelu'}
</div>
</div>
`;
}).join('');
}
// Update the eventContent function to add vehicle-specific styling
// (Already defined in calendarConfig above, so this duplicate is removed)
// Call updateReservationsList when events change
calendar.on('eventAdd', updateReservationsList);
calendar.on('eventRemove', updateReservationsList);
calendar.on('eventChange', updateReservationsList);
// Initial update of reservations list
updateReservationsList();
// Vehicle select change handler
document.getElementById('vehicle').addEventListener('change', async function() {
const startDate = document.getElementById('startDate').value;
const startTime = document.getElementById('startTime').value;
const endDate = document.getElementById('endDate').value;
const endTime = document.getElementById('endTime').value;
if (startDate && startTime && endDate && endTime) {
await checkVehicleAvailability(this.value, startDate, endDate);
}
});
// Check vehicle availability // Check vehicle availability
async function checkVehicleAvailability(vehicle, startDate, endDate) { async function checkVehicleAvailability(vehicle, startDate, endDate) {
try { try {
@@ -1025,8 +1361,16 @@
reservationModal.style.display = 'block'; reservationModal.style.display = 'block';
}); });
// Rest of the JavaScript code // Initialize time dropdowns when page loads
// ...existing code... document.addEventListener('DOMContentLoaded', function() {
populateTimeDropdowns();
// Set default dates to today
const today = new Date();
const dateStr = today.toISOString().split('T')[0];
document.getElementById('startDate').value = dateStr;
document.getElementById('endDate').value = dateStr;
});
}); });
</script> </script>
</body> </body>