This commit is contained in:
Tomas Dvorak
2025-06-17 07:16:20 +02:00
parent 1b424d4f5d
commit df23197f89
+210 -94
View File
@@ -582,6 +582,40 @@
color: #9a3412; color: #9a3412;
border: 1px solid #fed7aa; border: 1px solid #fed7aa;
} }
/* Vehicle Legend and Filters */
#vehicleFilters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.vehicle-filter-btn {
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.7;
border: 2px solid transparent;
border-radius: 9999px;
}
.vehicle-filter-btn:hover {
opacity: 1;
transform: translateY(-1px);
}
.vehicle-filter-btn.active {
opacity: 1;
border-color: #004990;
}
.reservation-item.hidden-vehicle {
display: none;
}
.fc-event.hidden-vehicle {
display: none;
}
</style> </style>
</head> </head>
<body class="bg-brand-gray min-h-screen"> <body class="bg-brand-gray min-h-screen">
@@ -635,33 +669,40 @@
<!-- Main Content --> <!-- Main Content -->
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="calendar-container"> <div class="calendar-container">
<!-- Vehicle Legend --> <div class="bg-white p-4 mb-4 rounded-lg shadow-sm"> <!-- Vehicle Legend and Filters -->
<div class="bg-white p-4 mb-4 rounded-lg shadow-sm">
<h3 class="text-lg font-semibold mb-3">Přehled vozidel</h3> <h3 class="text-lg font-semibold mb-3">Přehled vozidel</h3>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3" id="vehicleFilters">
<div class="flex items-center"> <button class="vehicle-filter-btn active" data-vehicle="all">
<div class="w-4 h-4 rounded-full bg-blue-500 mr-2"></div> <span class="vehicle-badge bg-gray-100 text-gray-700">
<span>VW Caddy - 4Z1 8241</span> <i class="fas fa-car"></i>Všechna vozidla
</div> </span>
<div class="flex items-center"> </button>
<div class="w-4 h-4 rounded-full bg-green-500 mr-2"></div> <button class="vehicle-filter-btn" data-vehicle="VW Caddy - 4Z1 8241">
<span>VW Golf - 5Z5 8694</span> <span class="vehicle-badge vehicle-vw-caddy">
</div> <i class="fas fa-truck"></i>VW Caddy - 4Z1 8241
<div class="flex items-center"> </span>
<div class="w-4 h-4 rounded-full bg-orange-500 mr-2"></div> </button>
<span>Škoda Fabia - 1Z3 5789</span> <button class="vehicle-filter-btn" data-vehicle="VW Golf - 5Z5 8694">
</div> <span class="vehicle-badge vehicle-vw-golf">
<div class="flex items-center"> <i class="fas fa-car"></i>VW Golf - 5Z5 8694
<div class="w-4 h-4 rounded-full bg-purple-500 mr-2"></div> </span>
<span>BMW 218d - 6Z5 4739</span> </button>
</div> <button class="vehicle-filter-btn" data-vehicle="Škoda Fabia - 1Z3 5789">
<div class="flex items-center"> <span class="vehicle-badge vehicle-skoda-fabia">
<div class="w-4 h-4 rounded-full bg-indigo-500 mr-2"></div> <i class="fas fa-car"></i>Škoda Fabia - 1Z3 5789
<span>BMW 218d - 6Z5 4740</span> </span>
</div> </button>
<div class="flex items-center"> <button class="vehicle-filter-btn" data-vehicle="BMW 218d - 6Z5 4739">
<div class="w-4 h-4 rounded-full bg-red-500 mr-2"></div> <span class="vehicle-badge vehicle-bmw-218d">
<span>Škoda Superb - 2BY 2398</span> <i class="fas fa-car-side"></i>BMW 218d - 6Z5 4739
</div> </span>
</button>
<button class="vehicle-filter-btn" data-vehicle="Škoda Superb - 2BY 2398">
<span class="vehicle-badge vehicle-skoda-superb">
<i class="fas fa-car-side"></i>Škoda Superb - 2BY 2398
</span>
</button>
</div> </div>
</div> </div>
@@ -864,30 +905,54 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
let calendar; // Helper functions for date/time formatting
const reservationForm = document.getElementById('reservationForm'); function formatDateForAPI(date) {
const statusMessage = document.getElementById('statusMessage'); return date.toISOString().split('T')[0];
const modal = document.getElementById('eventModal'); }
const highTrafficWarning = document.getElementById('highTrafficWarning');
const availabilityStatus = document.getElementById('availabilityStatus'); function formatTimeForAPI(date) {
let currentEventId = null; return date.getHours().toString().padStart(2, '0') + ':' +
date.getMinutes().toString().padStart(2, '0');
}
// Parse date and time inputs into a Date object
function parseDateTimeInputs(dateInput, timeInput) {
const [hours, minutes] = timeInput.value.split(':');
const date = new Date(dateInput.value);
date.setHours(parseInt(hours, 10), parseInt(minutes, 10), 0, 0);
return date;
}
// Function to check availability and traffic
async function checkAvailabilityAndTraffic() { async function checkAvailabilityAndTraffic() {
const vehicle = document.getElementById('vehicle').value; const vehicle = document.getElementById('vehicle').value;
const startDate = document.getElementById('startDate').value; const startDateInput = document.getElementById('startDate');
const startTime = document.getElementById('startTime').value; const startTimeInput = document.getElementById('startTime');
const endDate = document.getElementById('endDate').value; const endDateInput = document.getElementById('endDate');
const endTime = document.getElementById('endTime').value; const endTimeInput = document.getElementById('endTime');
if (!vehicle || !startDate || !startTime || !endDate || !endTime) { if (!vehicle || !startDateInput.value || !startTimeInput.value ||
!endDateInput.value || !endTimeInput.value) {
return; return;
} }
try { try {
const response = await fetch(`/api/check-availability?vehicle=${encodeURIComponent(vehicle)}&startDate=${startDate}&startTime=${startTime}&endDate=${endDate}&endTime=${endTime}`); const startDateTime = parseDateTimeInputs(startDateInput, startTimeInput);
const data = await response.json(); const endDateTime = parseDateTimeInputs(endDateInput, endTimeInput);
const response = await fetch(`/api/check-availability?` +
`vehicle=${encodeURIComponent(vehicle)}&` +
`startDate=${formatDateForAPI(startDateTime)}&` +
`startTime=${formatTimeForAPI(startDateTime)}&` +
`endDate=${formatDateForAPI(endDateTime)}&` +
`endTime=${formatTimeForAPI(endDateTime)}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Update availability status // Update availability status
availabilityStatus.classList.remove('hidden', 'bg-green-50', 'text-green-700', 'bg-red-50', 'text-red-700'); availabilityStatus.classList.remove('hidden', 'bg-green-50', 'text-green-700', 'bg-red-50', 'text-red-700');
@@ -896,19 +961,10 @@
availabilityStatus.innerHTML = '<i class="fas fa-check-circle mr-2"></i>Vozidlo je v tomto čase k dispozici'; availabilityStatus.innerHTML = '<i class="fas fa-check-circle mr-2"></i>Vozidlo je v tomto čase k dispozici';
} else { } else {
availabilityStatus.classList.add('bg-red-50', 'text-red-700'); 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.innerHTML = '<i class="fas fa-times-circle mr-2"></i>Vozidlo není v tomto čase k dispozici';
} }
availabilityStatus.classList.remove('hidden'); 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 // Disable submit button if not available
const submitButton = reservationForm.querySelector('button[type="submit"]'); const submitButton = reservationForm.querySelector('button[type="submit"]');
submitButton.disabled = !data.available; submitButton.disabled = !data.available;
@@ -918,6 +974,7 @@
return data.available; return data.available;
} catch (error) { } catch (error) {
console.error('Error checking availability:', error); console.error('Error checking availability:', error);
availabilityStatus.classList.add('hidden');
return false; return false;
} }
} }
@@ -925,21 +982,20 @@
// Add event listeners for form inputs // Add event listeners for form inputs
const formInputs = ['vehicle', 'startDate', 'startTime', 'endDate', 'endTime']; const formInputs = ['vehicle', 'startDate', 'startTime', 'endDate', 'endTime'];
formInputs.forEach(inputId => { formInputs.forEach(inputId => {
document.getElementById(inputId).addEventListener('change', checkAvailabilityAndTraffic); document.getElementById(inputId)?.addEventListener('change', checkAvailabilityAndTraffic);
}); });
// Handle form submission // Handle form submission
reservationForm.addEventListener('submit', async function(e) { reservationForm.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const startDate = document.getElementById('startDate').value; const startDateInput = document.getElementById('startDate');
const startTime = document.getElementById('startTime').value; const startTimeInput = document.getElementById('startTime');
const endDate = document.getElementById('endDate').value; const endDateInput = document.getElementById('endDate');
const endTime = document.getElementById('endTime').value; const endTimeInput = document.getElementById('endTime');
// Create ISO datetime strings const startDateTime = parseDateTimeInputs(startDateInput, startTimeInput);
const startDateTime = new Date(`${startDate}T${startTime}`); const endDateTime = parseDateTimeInputs(endDateInput, endTimeInput);
const endDateTime = new Date(`${endDate}T${endTime}`);
// Validate dates // Validate dates
if (endDateTime <= startDateTime) { if (endDateTime <= startDateTime) {
@@ -948,11 +1004,7 @@
} }
// Check availability one last time // Check availability one last time
const isAvailable = await checkVehicleAvailability( const isAvailable = await checkAvailabilityAndTraffic();
document.getElementById('vehicle').value,
startDateTime,
endDateTime
);
if (!isAvailable) { if (!isAvailable) {
showMessage('Vozidlo již není k dispozici v tomto čase', 'error'); showMessage('Vozidlo již není k dispozici v tomto čase', 'error');
@@ -963,10 +1015,10 @@
const reservationData = { const reservationData = {
driverName: document.getElementById('driverName').value, driverName: document.getElementById('driverName').value,
vehicle: document.getElementById('vehicle').value, vehicle: document.getElementById('vehicle').value,
startDate, startDate: formatDateForAPI(startDateTime),
startTime, startTime: formatTimeForAPI(startDateTime),
endDate, endDate: formatDateForAPI(endDateTime),
endTime, endTime: formatTimeForAPI(endDateTime),
purpose: document.getElementById('purpose')?.value || '' purpose: document.getElementById('purpose')?.value || ''
}; };
@@ -988,29 +1040,24 @@
// Add the new event to the calendar // Add the new event to the calendar
calendar.addEvent({ calendar.addEvent({
id: result.id, id: result.id,
title: reservationData.driverName, title: `${result.vehicle} - ${result.driverName}`,
start: startDateTime, start: `${result.startDate}T${result.startTime}`,
end: endDateTime, end: `${result.endDate}T${result.endTime}`,
extendedProps: { extendedProps: {
driverName: reservationData.driverName, driverName: result.driverName,
vehicle: reservationData.vehicle, vehicle: result.vehicle,
purpose: reservationData.purpose purpose: result.purpose
} }
}); });
// Reset form and close modal // Reset form and close modal
reservationForm.reset(); reservationForm.reset();
document.getElementById('reservationModal').style.display = 'none'; document.getElementById('reservationModal').style.display = 'none';
// Show success message
showMessage('Rezervace byla úspěšně vytvořena', 'success'); showMessage('Rezervace byla úspěšně vytvořena', 'success');
// Update the reservations list
updateReservationsList();
} catch (error) { } catch (error) {
console.error('Error creating reservation:', error); console.error('Error:', error);
showMessage('Nepodařilo se vytvořit rezervaci. Zkuste to prosím znovu.', 'error'); showMessage('Nepodařilo se vytvořit rezervaci', 'error');
} }
}); });
@@ -1040,23 +1087,60 @@
} }
// Initialize calendar // Initialize calendar
const calendarEl = document.getElementById('calendar'); const calendarEl = document.getElementById('calendar');
function createEventContent(arg) { let selectedVehicle = 'all';
return {
html: ` // Filter events function
<div class="fc-event-main"> function filterEvents() {
<div class="fc-event-time">${arg.timeText}</div> const events = calendar.getEvents();
<div class="fc-event-title">${arg.event.extendedProps.driverName}</div> events.forEach(event => {
<div class="fc-event-desc text-sm">${arg.event.extendedProps.vehicle}</div> const eventEl = event.el;
</div> if (eventEl) {
` if (selectedVehicle === 'all' || event.extendedProps.vehicle === selectedVehicle) {
}; eventEl.classList.remove('hidden-vehicle');
} else {
eventEl.classList.add('hidden-vehicle');
}
}
});
// Filter reservation list
const reservationItems = document.querySelectorAll('#reservationsList .reservation-item');
reservationItems.forEach(item => {
const vehicleBadge = item.querySelector('.reservation-vehicle-badge');
if (vehicleBadge) {
const vehicle = vehicleBadge.textContent.trim();
if (selectedVehicle === 'all' || vehicle === selectedVehicle) {
item.classList.remove('hidden-vehicle');
} else {
item.classList.add('hidden-vehicle');
}
}
});
// Update active filter button
document.querySelectorAll('.vehicle-filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.vehicle === selectedVehicle);
});
} }
// Add click handlers for filter buttons
document.querySelectorAll('.vehicle-filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
selectedVehicle = btn.dataset.vehicle;
filterEvents();
});
});
// Calendar configuration // Calendar configuration
const calendarConfig = { const calendarConfig = {
eventDidMount: function(info) { eventDidMount: function(info) {
// Add vehicle-specific class for styling const vehicleClass = 'event-' + info.event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-');
const vehicleClass = 'event-' + info.event.extendedProps.vehicle.toLowerCase().replace(/[^a-z0-9]+/g, '-');
info.el.classList.add(vehicleClass); info.el.classList.add(vehicleClass);
// Apply initial filtering
if (selectedVehicle !== 'all' && info.event.extendedProps.vehicle !== selectedVehicle) {
info.el.classList.add('hidden-vehicle');
}
}, initialView: 'dayGridMonth', }, initialView: 'dayGridMonth',
headerToolbar: { headerToolbar: {
left: 'prev,next today', left: 'prev,next today',
@@ -1218,8 +1302,9 @@
const html = currentAndFutureEvents.map(event => { const html = currentAndFutureEvents.map(event => {
const vehicleClass = 'vehicle-' + event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-'); const vehicleClass = 'vehicle-' + event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-');
const isHidden = selectedVehicle !== 'all' && event.extendedProps.vehicle !== selectedVehicle ? 'hidden-vehicle' : '';
return ` return `
<div class="reservation-item hover:bg-gray-50"> <div class="reservation-item hover:bg-gray-50 ${isHidden}">
<span class="reservation-vehicle-badge ${vehicleClass} text-sm"> <span class="reservation-vehicle-badge ${vehicleClass} text-sm">
${event.extendedProps.vehicle} ${event.extendedProps.vehicle}
</span> </span>
@@ -1369,6 +1454,37 @@
modal.style.display = 'none'; modal.style.display = 'none';
} }
} }
// Vehicle filter functionality
const vehicleFilterButtons = document.querySelectorAll('.vehicle-filter-btn');
vehicleFilterButtons.forEach(button => {
button.addEventListener('click', function() {
const vehicle = this.getAttribute('data-vehicle');
// Update active button
vehicleFilterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Show/hide reservations based on filter
const reservations = document.querySelectorAll('.reservation-item');
reservations.forEach(item => {
if (vehicle === 'all' || item.querySelector('.reservation-vehicle-badge').textContent.trim() === vehicle) {
item.classList.remove('hidden-vehicle');
} else {
item.classList.add('hidden-vehicle');
}
});
// Show/hide calendar events based on filter
calendar.getEvents().forEach(event => {
if (vehicle === 'all' || event.extendedProps.vehicle === vehicle) {
event.setProp('classNames', []);
} else {
event.setProp('classNames', ['hidden-vehicle']);
}
});
});
});
}); });
</script> </script>
</body> </body>