mirror of
https://github.com/Dvorinka/PPve.git
synced 2026-06-03 20:12:59 +00:00
1887 lines
71 KiB
HTML
1887 lines
71 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="cs">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Poppe Potthoff - Rezervace služebních vozů</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
|
||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.css' rel='stylesheet' />
|
||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.js'></script>
|
||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales/cs.js'></script>
|
||
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
'brand-blue': '#004990',
|
||
'brand-light-blue': '#0072b0',
|
||
'brand-gray': '#f0f2f5'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* Existing styles */
|
||
.fc-event {
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
border: none;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.fc-event:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.fc-toolbar-title {
|
||
text-transform: capitalize;
|
||
font-weight: 600;
|
||
color: #2d3748;
|
||
}
|
||
|
||
/* Calendar navigation buttons */
|
||
.fc .fc-button {
|
||
border-radius: 0.375rem;
|
||
padding: 0.5rem 1rem;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.fc .fc-prev-button,
|
||
.fc .fc-next-button {
|
||
background-color: #ffffff;
|
||
border: 1px solid #e5e7eb;
|
||
color: #374151;
|
||
}
|
||
|
||
.fc .fc-prev-button:hover,
|
||
.fc .fc-next-button:hover {
|
||
background-color: #f3f4f6;
|
||
border-color: #d1d5db;
|
||
}
|
||
|
||
.fc .fc-today-button {
|
||
background-color: #004990;
|
||
border-color: #004990;
|
||
}
|
||
|
||
.fc .fc-today-button:hover {
|
||
background-color: #003970;
|
||
border-color: #003970;
|
||
}
|
||
|
||
/* Improved calendar header */
|
||
.fc .fc-toolbar-title {
|
||
font-size: 1.25rem;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
/* Active state for view buttons */
|
||
.fc .fc-button-active {
|
||
background-color: #004990 !important;
|
||
border-color: #004990 !important;
|
||
color: white !important;
|
||
}
|
||
|
||
/* Space between button groups */
|
||
.fc .fc-button-group {
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.fc .fc-toolbar.fc-header-toolbar {
|
||
margin-bottom: 1.5rem;
|
||
padding: 0.5rem;
|
||
background-color: white;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.fc .fc-button-primary {
|
||
background-color: #004990;
|
||
border-color: #004990;
|
||
text-transform: uppercase;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
padding: 0.5rem 1rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.fc .fc-button-primary:hover {
|
||
background-color: #0072b0;
|
||
border-color: #0072b0;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||
.fc .fc-button-primary:not(:disabled):active {
|
||
background-color: #0072b0;
|
||
border-color: #0072b0;
|
||
}
|
||
|
||
.fc-view-harness {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.fc-scrollgrid {
|
||
border: none !important;
|
||
}
|
||
|
||
.fc-col-header-cell {
|
||
background: #f8fafc;
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.fc-day-today {
|
||
background: #e8f4ff !important;
|
||
}
|
||
|
||
.fc-timegrid-slot {
|
||
height: 2rem !important;
|
||
}
|
||
|
||
.fc-timegrid-slot-label {
|
||
font-size: 0.75rem !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.reservation-card {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.reservation-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* Vehicle badges */
|
||
.vehicle-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 9999px;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
.vehicle-badge i {
|
||
margin-right: 0.5rem;
|
||
}
|
||
|
||
/* Vehicle-specific colors */
|
||
.vehicle-vw-caddy { background-color: #e6f3ff; color: #1a73e8; }
|
||
.vehicle-vw-golf { background-color: #e6ffe6; color: #28a745; }
|
||
.vehicle-skoda-fabia { background-color: #fff3e6; color: #fd7e14; }
|
||
.vehicle-bmw-218d { background-color: #f3e6ff; color: #6f42c1; }
|
||
.vehicle-skoda-superb { background-color: #ffe6e6; color: #dc3545; }
|
||
|
||
/* Calendar size adjustments */
|
||
.calendar-container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
padding: 1rem;
|
||
}
|
||
|
||
/* Responsive calendar styles */
|
||
@media (max-width: 768px) {
|
||
.fc .fc-toolbar {
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.fc .fc-toolbar-title {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.fc-header-toolbar {
|
||
padding: 0.5rem !important;
|
||
}
|
||
|
||
.fc-view-harness {
|
||
height: 400px !important;
|
||
}
|
||
|
||
.fc .fc-button {
|
||
padding: 0.4rem 0.8rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
}
|
||
|
||
/* Adjust calendar size for desktop */
|
||
.fc-view-harness {
|
||
height: 450px !important;
|
||
}
|
||
|
||
/* Improve event styling */
|
||
.fc-event {
|
||
padding: 2px 4px;
|
||
margin: 1px 0;
|
||
border-radius: 4px;
|
||
border: none !important;
|
||
background: #fff;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.fc-event-title {
|
||
font-weight: 500;
|
||
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;
|
||
color: 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---4z1-8241 {
|
||
background-color: rgb(59, 130, 246) !important; /* blue-500 */
|
||
}
|
||
|
||
.event-vw-golf---5z5-8694 {
|
||
background-color: rgb(34, 197, 94) !important; /* green-500 */
|
||
}
|
||
|
||
.event-skoda-fabia---1z3-5789 {
|
||
background-color: rgb(249, 115, 22) !important; /* orange-500 */
|
||
}
|
||
|
||
.event-bmw-218d---6z5-4739 {
|
||
background-color: rgb(168, 85, 247) !important; /* purple-500 */
|
||
}
|
||
|
||
.event-bmw-218d---6z5-4740 {
|
||
background-color: rgb(139, 92, 246) !important; /* purple-400 - slightly lighter for distinction */
|
||
}
|
||
|
||
.event-skoda-superb---2by-2398 {
|
||
background-color: rgb(239, 68, 68) !important; /* red-500 */
|
||
}
|
||
|
||
/* 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 {
|
||
max-width: 600px;
|
||
margin: 2rem auto;
|
||
padding: 2rem;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* Form group spacing */
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
/* Form field styles */
|
||
.form-group select {
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||
background-position: right 0.5rem center;
|
||
background-repeat: no-repeat;
|
||
background-size: 1.5em 1.5em;
|
||
}
|
||
|
||
.form-group .select-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.form-group .select-icon {
|
||
position: absolute;
|
||
left: 0.75rem;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #6b7280;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 0.5rem 2rem 0.5rem 2.5rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
background-color: white;
|
||
color: #111827;
|
||
}
|
||
|
||
.form-group select:focus {
|
||
outline: 2px solid transparent;
|
||
outline-offset: 2px;
|
||
border-color: #004990;
|
||
box-shadow: 0 0 0 1px #004990;
|
||
}
|
||
|
||
.form-group select option {
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
/* Time input styling */
|
||
.time-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.time-input-wrapper i {
|
||
position: absolute;
|
||
left: 0.75rem;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #6b7280;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.time-input-wrapper input[type="time"] {
|
||
width: 100%;
|
||
padding: 0.5rem 0.75rem 0.5rem 2.5rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
appearance: none;
|
||
background-color: white;
|
||
color: #111827;
|
||
}
|
||
|
||
.time-input-wrapper input[type="time"]:focus {
|
||
outline: 2px solid transparent;
|
||
outline-offset: 2px;
|
||
border-color: #004990;
|
||
box-shadow: 0 0 0 1px #004990;
|
||
}
|
||
|
||
/* Hide default time picker icon in Edge/Chrome */
|
||
.time-input-wrapper input[type="time"]::-webkit-calendar-picker-indicator {
|
||
background: none;
|
||
padding-right: 0.5rem;
|
||
}
|
||
|
||
/* Style for Firefox */
|
||
.time-input-wrapper input[type="time"]::-moz-calendar-picker-indicator {
|
||
background: none;
|
||
}
|
||
|
||
/* Responsive form layout */
|
||
@media (max-width: 640px) {
|
||
.form-container {
|
||
padding: 1rem;
|
||
margin: 1rem;
|
||
}
|
||
|
||
.grid-cols-2 {
|
||
grid-template-columns: 1fr !important;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
}
|
||
|
||
/* Improve button responsiveness */
|
||
.btn {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border-radius: 0.5rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
@media (min-width: 640px) {
|
||
.btn {
|
||
width: auto;
|
||
}
|
||
}
|
||
|
||
/* Modal styles */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
padding: 1rem;
|
||
}
|
||
|
||
.modal-content {
|
||
background-color: white;
|
||
max-width: 500px;
|
||
margin: 2rem auto;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 0.75rem;
|
||
right: 0.75rem;
|
||
cursor: pointer;
|
||
font-size: 1.5rem;
|
||
color: #666;
|
||
}
|
||
|
||
.modal-header {
|
||
border-bottom: 1px solid #e5e7eb;
|
||
padding-bottom: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.modal-body {
|
||
color: #374151;
|
||
}
|
||
|
||
.info-grid {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 0.75rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.info-label {
|
||
font-weight: 500;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.delete-btn {
|
||
background-color: #ef4444;
|
||
color: white;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 0.375rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
background-color: #dc2626;
|
||
}
|
||
|
||
/* Reservation form modal styles */
|
||
.reservation-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
padding: 2rem;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.reservation-modal .form-container {
|
||
background: white;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
padding: 2rem;
|
||
}
|
||
|
||
.reservation-modal .close-button {
|
||
position: absolute;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
font-size: 1.5rem;
|
||
color: #666;
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.reservation-modal .close-button:hover {
|
||
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;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* Clickable reservations styling */
|
||
.reservation-item {
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.reservation-item:hover {
|
||
background-color: #f3f4f6;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
/* Modal delete button styling */
|
||
#deleteReservation {
|
||
background-color: #ef4444;
|
||
color: white;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 0.375rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-top: 1rem;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
#deleteReservation:hover {
|
||
background-color: #dc2626;
|
||
}
|
||
|
||
#deleteReservation.hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* New styles for improved layout and responsiveness */
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #f9fafb;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
h1, h2, h3, h4, h5, h6 {
|
||
margin: 0;
|
||
padding: 0;
|
||
color: #111827;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.875rem;
|
||
font-weight: 700;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
h3 {
|
||
font-size: 1.25rem;
|
||
font-weight: 500;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
p {
|
||
margin: 0;
|
||
padding: 0;
|
||
color: #374151;
|
||
line-height: 1.625;
|
||
}
|
||
|
||
a {
|
||
color: #004990;
|
||
text-decoration: none;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
a:hover {
|
||
color: #0072b0;
|
||
}
|
||
|
||
/* Button styles */
|
||
.btn {
|
||
display: inline-block;
|
||
padding: 0.75rem 1.5rem;
|
||
border-radius: 0.375rem;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, transform 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #004990;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #0072b0;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #f3f4f6;
|
||
color: #111827;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #e5e7eb;
|
||
}
|
||
|
||
/* Card styles */
|
||
.card {
|
||
background: white;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.card-header {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.card-subtitle {
|
||
font-size: 0.875rem;
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* Table styles */
|
||
.table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.table th, .table td {
|
||
padding: 0.75rem;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.table th {
|
||
background-color: #f8fafc;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Badge styles */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 0.375rem 0.75rem;
|
||
border-radius: 9999px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
}
|
||
|
||
.badge-success {
|
||
background-color: #d1fae5;
|
||
color: #065f46;
|
||
}
|
||
|
||
.badge-danger {
|
||
background-color: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
|
||
/* Spinner styles */
|
||
.spinner {
|
||
border: 2px solid #e5e7eb;
|
||
border-top: 2px solid #004990;
|
||
border-radius: 50%;
|
||
width: 1rem;
|
||
height: 1rem;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Toast notification styles */
|
||
.toast {
|
||
position: fixed;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
background: white;
|
||
border-radius: 0.375rem;
|
||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||
padding: 1rem;
|
||
z-index: 1000;
|
||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||
}
|
||
|
||
.toast.hide {
|
||
transform: translateY(-10px);
|
||
opacity: 0;
|
||
}
|
||
|
||
.toast.show {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Responsive styles */
|
||
@media (max-width: 768px) {
|
||
.calendar-container {
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.form-container {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.fc-view-harness {
|
||
height: 350px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.form-container {
|
||
padding: 1rem;
|
||
margin: 1rem;
|
||
}
|
||
|
||
.grid-cols-2 {
|
||
grid-template-columns: 1fr !important;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-brand-gray min-h-screen">
|
||
<!-- Nav bar copied from existing template -->
|
||
<nav class="bg-brand-blue text-white shadow-lg">
|
||
<div class="max-w-6xl mx-auto px-4 py-3 flex justify-between items-center">
|
||
<div class="flex items-center space-x-2">
|
||
<a href="http://webportal/index.html"><img src="http://pp-kunovice.cz/wp-content/uploads/2022/04/logo-retina-white.png" alt="Poppe Potthoff Logo" class="h-10"></a>
|
||
<div class="hidden md:block text-xl font-semibold">Poppe + Potthoff</div>
|
||
</div>
|
||
|
||
<!-- Mobile menu button -->
|
||
<div class="md:hidden">
|
||
<button id="mobile-menu-button" class="text-white focus:outline-none">
|
||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Desktop menu -->
|
||
<div class="hidden md:flex space-x-6">
|
||
<a href="http://webportal/index.html" class="hover:text-brand-light-blue">Rozcestník</a>
|
||
<a href="http://webportal/evidence-aut" class="hover:text-brand-light-blue">Evidence aut</a>
|
||
<a href="http://ppc-app/pwkweb2/" class="hover:text-brand-light-blue">Obědy</a>
|
||
<a href="http://osticket/" class="hover:text-brand-light-blue">OSticket</a>
|
||
<a href="http://kanboard/" class="hover:text-brand-light-blue">Kanboard</a>
|
||
<a href="http://webportal:8080" class="hover:text-brand-light-blue">Kontakt</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mobile menu -->
|
||
<div id="mobile-menu" class="hidden md:hidden px-2 pt-2 pb-3 space-y-1">
|
||
<a href="webportal/index.html" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Rozcestník</a>
|
||
<a href="webportal/evidence-aut" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Evidence aut</a>
|
||
<a href="http://ppc-app/pwkweb2/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Obědy</a>
|
||
<a href="http://osticket/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">OSticket</a>
|
||
<a href="http://kanboard/" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kanboard</a>
|
||
<a href="http://webportal:8080" class="block px-3 py-2 rounded-md text-base font-medium hover:text-brand-light-blue">Kontakt</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Page Header -->
|
||
<div class="bg-gradient-to-r from-brand-blue to-brand-light-blue text-white py-6 mb-8">
|
||
<div class="max-w-6xl mx-auto px-4">
|
||
<h1 class="text-3xl font-bold">Poppe + Potthoff – Rezervace služebních vozů</h1>
|
||
<p class="text-gray-100 mt-2">Systém pro rezervaci služebních vozidel</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content -->
|
||
<div class="container mx-auto px-4 py-8">
|
||
<div class="calendar-container">
|
||
<!-- 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>
|
||
<div class="flex flex-wrap gap-3" id="vehicleFilters">
|
||
<button class="vehicle-filter-btn active" data-vehicle="all">
|
||
<span class="vehicle-badge bg-gray-100 text-gray-700">
|
||
<i class="fas fa-car"></i>Všechna vozidla
|
||
</span>
|
||
</button>
|
||
<button class="vehicle-filter-btn" data-vehicle="VW Caddy - 4Z1 8241">
|
||
<span class="vehicle-badge vehicle-vw-caddy">
|
||
<i class="fas fa-truck"></i>VW Caddy - 4Z1 8241
|
||
</span>
|
||
</button>
|
||
<button class="vehicle-filter-btn" data-vehicle="VW Golf - 5Z5 8694">
|
||
<span class="vehicle-badge vehicle-vw-golf">
|
||
<i class="fas fa-car"></i>VW Golf - 5Z5 8694
|
||
</span>
|
||
</button>
|
||
<button class="vehicle-filter-btn" data-vehicle="Škoda Fabia - 1Z3 5789">
|
||
<span class="vehicle-badge vehicle-skoda-fabia">
|
||
<i class="fas fa-car"></i>Škoda Fabia - 1Z3 5789
|
||
</span>
|
||
</button> <button class="vehicle-filter-btn" data-vehicle="BMW 218d - 6Z5 4739">
|
||
<span class="vehicle-badge vehicle-bmw-218d">
|
||
<i class="fas fa-car-side"></i>BMW 218d - 6Z5 4739
|
||
</span>
|
||
</button>
|
||
<button class="vehicle-filter-btn" data-vehicle="BMW 218d - 6Z5 4740">
|
||
<span class="vehicle-badge vehicle-bmw-218d">
|
||
<i class="fas fa-car-side"></i>BMW 218d - 6Z5 4740
|
||
</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>
|
||
|
||
<!-- Info Banner -->
|
||
<div class="bg-blue-50 border-l-4 border-brand-blue p-4 mb-4 rounded-r-lg shadow-sm">
|
||
<div class="flex">
|
||
<div class="flex-shrink-0">
|
||
<i class="fas fa-info-circle text-brand-blue"></i>
|
||
</div>
|
||
<div class="ml-3">
|
||
<p class="text-sm text-gray-700">
|
||
Pro rychlé vytvoření rezervace dvakrát klikněte na požadovaný čas/den v kalendáři.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id='calendar'></div> <div class="flex justify-center mt-6">
|
||
<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">
|
||
<i class="fas fa-plus-circle"></i>
|
||
Vytvořit novou rezervaci
|
||
</button>
|
||
</div> <!-- Reservations List -->
|
||
<div class="reservations-list mt-8">
|
||
<div class="reservations-list-header flex justify-between items-center">
|
||
<h3 class="text-lg font-semibold text-gray-800">Aktuální a budoucí rezervace</h3>
|
||
<div class="text-sm text-gray-500" id="reservationsCount"></div>
|
||
</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 class="form-container">
|
||
<button type="button" class="close-button" id="closeReservationModal">×</button>
|
||
<h2 class="text-2xl font-bold mb-6 text-gray-800">Nová rezervace</h2>
|
||
<form id="reservationForm" class="space-y-6"> <!-- Driver Name -->
|
||
<div class="form-group"> <label for="driverName">Jméno a příjmení řidiče</label>
|
||
<input type="text" id="driverName" name="driverName" required
|
||
class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-blue focus:border-transparent"
|
||
placeholder="např. Jan Novák"
|
||
pattern="^[A-Za-zÀ-ž]{2,}(\s[A-Za-zÀ-ž]{2,})+$"
|
||
title="Prosím zadejte celé jméno (jméno a příjmení)"
|
||
/>
|
||
<div id="nameError" class="text-red-500 text-sm mt-1 hidden">
|
||
Prosím zadejte celé jméno a příjmení
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Vehicle Selection with Warning -->
|
||
<div class="form-group">
|
||
<label for="vehicle">Vozidlo</label>
|
||
<div class="select-wrapper">
|
||
<i class="fas fa-car select-icon"></i>
|
||
<select id="vehicle" name="vehicle" required> <option value="" disabled selected>Vyberte vozidlo...</option>
|
||
<option value="VW Caddy - 4Z1 8241">VW Caddy - 4Z1 8241</option>
|
||
<option value="VW Golf - 5Z5 8694">VW Golf - 5Z5 8694</option>
|
||
<option value="Škoda Fabia - 1Z3 5789">Škoda Fabia - 1Z3 5789</option>
|
||
<option value="BMW 218d - 6Z5 4739">BMW 218d - 6Z5 4739</option>
|
||
<option value="BMW 218d - 6Z5 4740">BMW 218d - 6Z5 4740</option>
|
||
<option value="Škoda Superb - 2BY 2398">Škoda Superb - 2BY 2398</option>
|
||
</select>
|
||
</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>
|
||
|
||
<!-- Date and Time Selection -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<!-- Start Date and Time -->
|
||
<div class="form-group">
|
||
<label for="startDate">Datum začátku</label>
|
||
<input type="date" id="startDate" name="startDate" required class="w-full p-2 border border-gray-300 rounded-lg">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="startTime">Čas začátku</label>
|
||
<div class="time-input-wrapper">
|
||
<i class="fas fa-clock"></i>
|
||
<input type="time" id="startTime" name="startTime" required min="06:00" max="22:00">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<!-- End Date and Time -->
|
||
<div class="form-group">
|
||
<label for="endDate">Datum konce</label>
|
||
<input type="date" id="endDate" name="endDate" required class="w-full p-2 border border-gray-300 rounded-lg">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="endTime">Čas konce</label>
|
||
<div class="time-input-wrapper">
|
||
<i class="fas fa-clock"></i>
|
||
<input type="time" id="endTime" name="endTime" required min="06:00" max="22:00">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Purpose -->
|
||
<div class="space-y-2">
|
||
<label for="purpose" class="block text-sm font-medium text-gray-700">Účel jízdy (nepovinné)</label>
|
||
<div class="relative">
|
||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||
<i class="fas fa-briefcase text-gray-400"></i>
|
||
</div>
|
||
<input type="text" id="purpose" name="purpose"
|
||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-brand-light-blue focus:border-brand-light-blue">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<div class="mt-6 flex justify-end">
|
||
<button type="submit"
|
||
class="bg-brand-blue hover:bg-brand-light-blue text-white font-bold py-2 px-6 rounded-lg shadow transition-all duration-200">
|
||
Vytvořit rezervaci
|
||
</button>
|
||
</div>
|
||
|
||
</form>
|
||
|
||
<!-- Status message container -->
|
||
<div class="fixed bottom-4 right-4 z-50 hidden" id="statusMessage">
|
||
<div class="flex items-center shadow-lg rounded-lg px-4 py-3">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal HTML -->
|
||
<div id="eventModal" class="modal">
|
||
<div class="modal-content">
|
||
<span class="modal-close">×</span>
|
||
<div class="modal-header">
|
||
<h3 class="text-xl font-semibold text-gray-900">Detail rezervace</h3>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="info-grid">
|
||
<span class="info-label">Řidič:</span>
|
||
<span id="modalDriver"></span>
|
||
|
||
<span class="info-label">Vozidlo:</span>
|
||
<span id="modalVehicle"></span>
|
||
|
||
<span class="info-label">Začátek:</span>
|
||
<span id="modalStart"></span>
|
||
|
||
<span class="info-label">Konec:</span>
|
||
<span id="modalEnd"></span>
|
||
|
||
<span class="info-label">Účel:</span>
|
||
<span id="modalPurpose"></span>
|
||
</div>
|
||
<button id="deleteReservation" class="delete-btn hidden">
|
||
<i class="fas fa-trash mr-2"></i>Zrušit rezervaci
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer copied from existing template -->
|
||
<footer class="bg-gray-800 text-gray-400 py-8 mt-12">
|
||
<div class="max-w-6xl mx-auto px-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||
<!-- Company Info -->
|
||
<div>
|
||
<h3 class="text-white text-lg font-semibold mb-4">Poppe + Potthoff CZ</h3>
|
||
<p class="mb-2">IČO: 26902214</p>
|
||
<p class="mb-2">DIČ: CZ26902214</p>
|
||
<p class="mb-2">Schránka: gfrk5qy</p>
|
||
<p>Na Záhonech 1086, 686 04 Kunovice</p>
|
||
</div>
|
||
|
||
<!-- Quick Links -->
|
||
<div>
|
||
<h3 class="text-white text-lg font-semibold mb-4">Rychlé odkazy</h3>
|
||
<ul class="space-y-2">
|
||
<li><a href="http://webportal/" class="hover:text-white">Rozcestník</a></li>
|
||
<li><a href="http://webportal/evidence-aut" class="hover:text-white">Evidence aut</a></li>
|
||
<li><a href="http://ppc-app/pwkweb2/" class="hover:text-white">Objednávka obědů</a></li>
|
||
<li><a href="http://osticket/" class="hover:text-white">Technická podpora</a></li>
|
||
<li><a href="http://webportal:8080" class="hover:text-white">Kontakty</a></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Copyright -->
|
||
<div class="md:text-right">
|
||
<img src="http://pp-kunovice.cz/wp-content/uploads/2022/04/logo-retina-white.png" alt="Poppe Potthoff Logo" class="h-10 mb-4 inline-block">
|
||
<p class="text-sm"> 2025 Poppe + Potthoff CZ</p>
|
||
<p class="text-xs mt-2">Všechna práva vyhrazena</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border-t border-gray-700 mt-8 pt-6 text-center text-sm">
|
||
<p>Created by <a href="https://tdvorak.dev" class="text-blue-400 hover:text-blue-300">TDvorak</a></p>
|
||
</div>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Helper functions for date/time formatting
|
||
function formatDateForAPI(date) {
|
||
return date.toISOString().split('T')[0];
|
||
}
|
||
|
||
function formatTimeForAPI(date) {
|
||
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 for high traffic and show warning
|
||
function checkHighTraffic(vehicle, date) {
|
||
if (!vehicle || !date) return;
|
||
|
||
const events = calendar.getEvents();
|
||
const dayStart = new Date(date);
|
||
dayStart.setHours(0, 0, 0, 0);
|
||
const dayEnd = new Date(date);
|
||
dayEnd.setHours(23, 59, 59, 999);
|
||
|
||
// Count reservations for this vehicle on this day
|
||
const reservationsCount = events.filter(event => {
|
||
const eventStart = new Date(event.start);
|
||
return event.extendedProps.vehicle === vehicle &&
|
||
eventStart >= dayStart &&
|
||
eventStart <= dayEnd;
|
||
}).length;
|
||
|
||
const warningMessage = document.querySelector('#highTrafficWarning .warning-message');
|
||
const warningElement = document.getElementById('highTrafficWarning');
|
||
|
||
if (reservationsCount >= 2) {
|
||
warningMessage.textContent = `Upozornění: Toto vozidlo má již ${reservationsCount} rezervace v tento den`;
|
||
warningElement.classList.remove('hidden');
|
||
} else {
|
||
warningElement.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Add event listeners for checking high traffic
|
||
document.getElementById('vehicle').addEventListener('change', function(e) {
|
||
const startDate = document.getElementById('startDate').value;
|
||
if (startDate) {
|
||
checkHighTraffic(e.target.value, new Date(startDate));
|
||
}
|
||
});
|
||
|
||
document.getElementById('startDate').addEventListener('change', function(e) {
|
||
const vehicle = document.getElementById('vehicle').value;
|
||
if (vehicle) {
|
||
checkHighTraffic(vehicle, new Date(e.target.value));
|
||
}
|
||
});
|
||
|
||
async function checkAvailabilityAndTraffic() {
|
||
const vehicle = document.getElementById('vehicle').value;
|
||
const startDateInput = document.getElementById('startDate');
|
||
const startTimeInput = document.getElementById('startTime');
|
||
const endDateInput = document.getElementById('endDate');
|
||
const endTimeInput = document.getElementById('endTime');
|
||
|
||
if (!vehicle || !startDateInput.value || !startTimeInput.value ||
|
||
!endDateInput.value || !endTimeInput.value) {
|
||
return;
|
||
}
|
||
|
||
const startDateTime = parseDateTimeInputs(startDateInput, startTimeInput);
|
||
checkHighTraffic(vehicle, startDateTime);
|
||
|
||
try {
|
||
const response = await fetch(`/api/check-availability?` +
|
||
`vehicle=${encodeURIComponent(vehicle)}&` +
|
||
`startDate=${formatDateForAPI(startDateTime)}&` +
|
||
`startTime=${formatTimeForAPI(startDateTime)}&` +
|
||
`endDate=${formatDateForAPI(new Date(endDateInput.value))}&` +
|
||
`endTime=${endTimeInput.value}`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
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 není v tomto čase k dispozici';
|
||
}
|
||
availabilityStatus.classList.remove('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);
|
||
});
|
||
|
||
// Name validation function
|
||
function validateDriverName(name) {
|
||
// Check if name has at least two parts with minimum 2 characters each
|
||
const parts = name.trim().split(/\s+/);
|
||
return parts.length >= 2 && parts.every(part => part.length >= 2 && /^[A-Za-zÀ-ž]+$/.test(part));
|
||
}
|
||
|
||
// Add input validation for driver name
|
||
const driverNameInput = document.getElementById('driverName');
|
||
const nameError = document.getElementById('nameError');
|
||
|
||
driverNameInput.addEventListener('input', function() {
|
||
const isValid = validateDriverName(this.value);
|
||
this.classList.toggle('border-red-500', !isValid);
|
||
this.classList.toggle('border-gray-300', isValid);
|
||
nameError.classList.toggle('hidden', isValid);
|
||
});
|
||
|
||
// Handle form submission
|
||
reservationForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const driverName = document.getElementById('driverName').value;
|
||
if (!validateDriverName(driverName)) {
|
||
showMessage('Prosím zadejte platné jméno a příjmení', 'error');
|
||
return;
|
||
}
|
||
|
||
const startDateInput = document.getElementById('startDate');
|
||
const startTimeInput = document.getElementById('startTime');
|
||
const endDateInput = document.getElementById('endDate');
|
||
const endTimeInput = document.getElementById('endTime');
|
||
|
||
const startDateTime = parseDateTimeInputs(startDateInput, startTimeInput);
|
||
const endDateTime = parseDateTimeInputs(endDateInput, endTimeInput);
|
||
|
||
// Validate dates
|
||
if (endDateTime <= startDateTime) {
|
||
showMessage('Konec rezervace musí být po začátku', 'error');
|
||
return;
|
||
}
|
||
|
||
// Check availability one last time
|
||
const isAvailable = await checkAvailabilityAndTraffic();
|
||
|
||
if (!isAvailable) {
|
||
showMessage('Vozidlo již není k dispozici v tomto čase', 'error');
|
||
return;
|
||
}
|
||
|
||
// Prepare reservation data
|
||
const reservationData = {
|
||
driverName: document.getElementById('driverName').value,
|
||
vehicle: document.getElementById('vehicle').value,
|
||
startDate: formatDateForAPI(startDateTime),
|
||
startTime: formatTimeForAPI(startDateTime),
|
||
endDate: formatDateForAPI(endDateTime),
|
||
endTime: formatTimeForAPI(endDateTime),
|
||
purpose: document.getElementById('purpose')?.value || ''
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('/api/reservations', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(reservationData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// Store reservation ID in localStorage
|
||
const userReservations = JSON.parse(localStorage.getItem('userReservations') || '[]');
|
||
userReservations.push(result.id);
|
||
localStorage.setItem('userReservations', JSON.stringify(userReservations));
|
||
|
||
// Add the new event to the calendar
|
||
calendar.addEvent({
|
||
id: result.id,
|
||
title: `${result.vehicle} - ${result.driverName}`,
|
||
start: `${result.startDate}T${result.startTime}`,
|
||
end: `${result.endDate}T${result.endTime}`,
|
||
extendedProps: {
|
||
driverName: result.driverName,
|
||
vehicle: result.vehicle,
|
||
purpose: result.purpose
|
||
}
|
||
});
|
||
|
||
// Reset form and close modal
|
||
reservationForm.reset();
|
||
document.getElementById('reservationModal').style.display = 'none';
|
||
showMessage('Rezervace byla úspěšně vytvořena', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
showMessage('Nepodařilo se vytvořit rezervaci', 'error');
|
||
}
|
||
});
|
||
|
||
// Show status message function
|
||
function showMessage(text, type = 'success') {
|
||
const statusContainer = document.getElementById('statusMessage');
|
||
statusContainer.className = 'mt-4 p-4 rounded-md';
|
||
statusContainer.classList.remove('hidden');
|
||
|
||
if (type === 'success') {
|
||
statusContainer.classList.add('bg-green-50', 'text-green-800');
|
||
} else {
|
||
statusContainer.classList.add('bg-red-50', 'text-red-800');
|
||
}
|
||
|
||
statusContainer.innerHTML = `
|
||
<div class="flex items-center">
|
||
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'} mr-2"></i>
|
||
<span>${text}</span>
|
||
</div>
|
||
`;
|
||
|
||
// Auto hide after 5 seconds
|
||
setTimeout(() => {
|
||
statusContainer.classList.add('hidden');
|
||
}, 5000);
|
||
}
|
||
// Initialize calendar
|
||
const calendarEl = document.getElementById('calendar');
|
||
let selectedVehicle = 'all';
|
||
let calendar; // Make calendar global
|
||
|
||
// Fix calendar initialization
|
||
const calendarConfig = {
|
||
initialView: 'dayGridMonth',
|
||
headerToolbar: {
|
||
left: 'prev,next today',
|
||
center: 'title',
|
||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||
},
|
||
validRange: {
|
||
start: new Date().toISOString().split('T')[0]
|
||
},
|
||
locale: 'cs',
|
||
slotMinTime: '06:00:00',
|
||
slotMaxTime: '22:00:00',
|
||
slotDuration: '01:00:00',
|
||
snapDuration: '01:00:00',
|
||
allDaySlot: false,
|
||
events: '/api/reservations',
|
||
eventDisplay: 'block',
|
||
eventTimeFormat: {
|
||
hour12: false
|
||
},
|
||
eventDidMount: function(info) {
|
||
const vehicle = info.event.extendedProps.vehicle;
|
||
const vehicleClass = 'event-' + vehicle.toLowerCase().replace(/\s+/g, '-');
|
||
|
||
// Apply vehicle-specific styling
|
||
info.el.classList.add(vehicleClass);
|
||
|
||
// Apply initial filtering
|
||
if (selectedVehicle !== 'all' && vehicle !== selectedVehicle) {
|
||
info.el.style.display = 'none';
|
||
info.el.style.visibility = 'hidden';
|
||
} else {
|
||
info.el.style.display = 'block';
|
||
info.el.style.visibility = 'visible';
|
||
}
|
||
},
|
||
eventSourceSuccess: function(content, xhr) {
|
||
// This runs after events are successfully loaded from the API
|
||
setTimeout(updateReservationsList, 0); // Use setTimeout to ensure DOM is ready
|
||
return content;
|
||
},
|
||
eventClassNames: function(arg) {
|
||
return ['event-' + arg.event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-')];
|
||
},
|
||
eventClick: function(info) {
|
||
showEventModal(info.event);
|
||
},
|
||
dateClick: function(info) {
|
||
showReservationForm(info.date);
|
||
}
|
||
};
|
||
|
||
calendar = new FullCalendar.Calendar(calendarEl, calendarConfig);
|
||
calendar.render();
|
||
|
||
// Fix reservation form show/hide
|
||
const reservationModal = document.getElementById('reservationModal');
|
||
const closeReservationModal = document.getElementById('closeReservationModal');
|
||
const newReservationBtn = document.getElementById('newReservationBtn');
|
||
|
||
newReservationBtn.addEventListener('click', () => {
|
||
showReservationForm(new Date());
|
||
});
|
||
|
||
closeReservationModal.addEventListener('click', () => {
|
||
reservationModal.style.display = 'none';
|
||
});
|
||
|
||
// Fix calendar filtering
|
||
function filterEvents() {
|
||
const events = calendar.getEvents();
|
||
events.forEach(event => {
|
||
const eventEl = event.el;
|
||
if (eventEl) {
|
||
if (selectedVehicle === 'all' || event.extendedProps.vehicle === selectedVehicle) {
|
||
eventEl.style.display = 'block';
|
||
eventEl.style.visibility = 'visible';
|
||
} else {
|
||
eventEl.style.display = 'none';
|
||
eventEl.style.visibility = 'hidden';
|
||
}
|
||
}
|
||
});
|
||
updateReservationsList();
|
||
}
|
||
|
||
// Update vehicle filter buttons to show active state
|
||
document.querySelectorAll('.vehicle-filter-btn').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
// Update active state
|
||
document.querySelectorAll('.vehicle-filter-btn').forEach(b => {
|
||
b.classList.remove('active');
|
||
b.querySelector('.vehicle-badge').classList.remove('ring-2', 'ring-brand-blue');
|
||
});
|
||
|
||
this.classList.add('active');
|
||
this.querySelector('.vehicle-badge').classList.add('ring-2', 'ring-brand-blue');
|
||
|
||
// Update selected vehicle and filter
|
||
selectedVehicle = this.dataset.vehicle;
|
||
filterEvents();
|
||
});
|
||
});
|
||
|
||
// Fix reservation form show function
|
||
function showReservationForm(date) {
|
||
const formattedDate = date.toISOString().split('T')[0];
|
||
const formattedTime = date.toTimeString().substring(0, 5);
|
||
|
||
document.getElementById('startDate').value = formattedDate;
|
||
document.getElementById('startTime').value = formattedTime;
|
||
|
||
const endDate = new Date(date);
|
||
endDate.setHours(endDate.getHours() + 1);
|
||
document.getElementById('endDate').value = formattedDate;
|
||
document.getElementById('endTime').value = endDate.toTimeString().substring(0, 5);
|
||
|
||
reservationModal.style.display = 'block';
|
||
}
|
||
|
||
// Close modal when clicking the close button
|
||
closeReservationModal.onclick = function() {
|
||
reservationModal.style.display = 'none';
|
||
}
|
||
|
||
// Ensure modal is defined before using it in the window.onclick handler
|
||
const modal = document.getElementById('eventModal');
|
||
|
||
// Close modal when clicking outside
|
||
window.onclick = function(event) {
|
||
if (event.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 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 reservationsCount = document.getElementById('reservationsCount');
|
||
const events = calendar.getEvents();
|
||
|
||
// Get current date at start of day
|
||
const now = new Date();
|
||
now.setHours(0, 0, 0, 0);
|
||
|
||
// Filter and sort events
|
||
const currentAndFutureEvents = events
|
||
.filter(event => event.end >= now)
|
||
.filter(event => selectedVehicle === 'all' || event.extendedProps.vehicle === selectedVehicle)
|
||
.sort((a, b) => a.start - b.start);
|
||
|
||
// Update count and list
|
||
if (currentAndFutureEvents.length === 0) {
|
||
reservationsCount.textContent = 'Žádné aktuální ani budoucí rezervace';
|
||
reservationsList.innerHTML = `
|
||
<div class="p-4 text-center text-gray-500">
|
||
Žádné aktuální ani budoucí rezervace
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
reservationsCount.textContent = `Počet rezervací: ${currentAndFutureEvents.length}`;
|
||
|
||
const html = currentAndFutureEvents.map(event => {
|
||
const vehicleClass = 'vehicle-' + event.extendedProps.vehicle.toLowerCase().replace(/\s+/g, '-');
|
||
return `
|
||
<div class="reservation-item hover:bg-gray-50" data-event-id="${event.id}">
|
||
<span class="reservation-vehicle-badge ${vehicleClass} text-sm">
|
||
${event.extendedProps.vehicle}
|
||
</span>
|
||
<div class="flex-grow">
|
||
<div class="font-medium">${event.extendedProps.driverName}</div>
|
||
<div class="text-sm text-gray-600">
|
||
${formatDateTime(event.start)} - ${formatDateTime(event.end)}
|
||
</div>
|
||
${event.extendedProps.purpose ? `
|
||
<div class="text-sm text-gray-500 mt-1">
|
||
Účel: ${event.extendedProps.purpose}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
reservationsList.innerHTML = html;
|
||
}
|
||
|
||
// Enhanced date/time formatting function
|
||
function formatDateTime(date) {
|
||
const options = {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
};
|
||
return new Date(date).toLocaleString('cs-CZ', options);
|
||
}
|
||
|
||
// Function to check for high traffic
|
||
async function checkHighTraffic(vehicle, date) {
|
||
if (!vehicle || !date) return;
|
||
|
||
const startOfDay = new Date(date);
|
||
startOfDay.setHours(0, 0, 0, 0);
|
||
|
||
const endOfDay = new Date(date);
|
||
endOfDay.setHours(23, 59, 59, 999);
|
||
|
||
const events = calendar.getEvents();
|
||
const vehicleEventsCount = events.filter(event =>
|
||
event.extendedProps.vehicle === vehicle &&
|
||
event.start >= startOfDay &&
|
||
event.start <= endOfDay
|
||
).length;
|
||
|
||
const warningEl = document.getElementById('highTrafficWarning');
|
||
if (vehicleEventsCount >= 3) {
|
||
warningEl.querySelector('.warning-message').textContent =
|
||
`Toto vozidlo má v daný den již ${vehicleEventsCount} rezervací.`;
|
||
warningEl.classList.remove('hidden');
|
||
} else {
|
||
warningEl.classList.add('hidden');
|
||
}
|
||
|
||
return vehicleEventsCount;
|
||
}
|
||
|
||
// Add event listeners for high traffic checking
|
||
document.getElementById('vehicle').addEventListener('change', function() {
|
||
const startDate = document.getElementById('startDate').value;
|
||
if (startDate) {
|
||
checkHighTraffic(this.value, new Date(startDate));
|
||
}
|
||
});
|
||
|
||
document.getElementById('startDate').addEventListener('change', function() {
|
||
const vehicle = document.getElementById('vehicle').value;
|
||
if (vehicle) {
|
||
checkHighTraffic(vehicle, new Date(this.value));
|
||
}
|
||
});
|
||
|
||
// Update reservations list when calendar events change
|
||
calendar.on('eventAdd', updateReservationsList);
|
||
calendar.on('eventRemove', updateReservationsList);
|
||
calendar.on('eventChange', updateReservationsList);
|
||
|
||
// Initial update of reservations list
|
||
calendar.on('eventSourceSuccess', updateReservationsList);
|
||
|
||
// Event modal functions
|
||
function showEventModal(event) {
|
||
currentEventId = event.id;
|
||
|
||
// Fill modal with event details
|
||
document.getElementById('modalDriver').textContent = event.extendedProps.driverName;
|
||
document.getElementById('modalVehicle').textContent = event.extendedProps.vehicle;
|
||
document.getElementById('modalStart').textContent = formatDateTime(event.start);
|
||
document.getElementById('modalEnd').textContent = formatDateTime(event.end);
|
||
document.getElementById('modalPurpose').textContent = event.extendedProps.purpose || '(není uvedeno)';
|
||
|
||
// Check if this reservation belongs to the current user
|
||
const userReservations = JSON.parse(localStorage.getItem('userReservations') || '[]');
|
||
const deleteButton = document.getElementById('deleteReservation');
|
||
|
||
if (userReservations.includes(event.id)) {
|
||
deleteButton.classList.remove('hidden');
|
||
} else {
|
||
deleteButton.classList.add('hidden');
|
||
}
|
||
|
||
// Show modal
|
||
document.getElementById('eventModal').style.display = 'block';
|
||
}
|
||
|
||
// Add click handlers for modal
|
||
document.querySelector('.modal-close').addEventListener('click', function() {
|
||
document.getElementById('eventModal').style.display = 'none';
|
||
});
|
||
|
||
window.addEventListener('click', function(event) {
|
||
const modal = document.getElementById('eventModal');
|
||
if (event.target === modal) {
|
||
modal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Handle delete button click
|
||
document.getElementById('deleteReservation').addEventListener('click', async function() {
|
||
if (!currentEventId) return;
|
||
|
||
if (confirm('Opravdu chcete zrušit tuto rezervaci?')) {
|
||
try {
|
||
const response = await fetch(`/api/reservations/${currentEventId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to delete reservation');
|
||
}
|
||
|
||
// Remove from localStorage
|
||
const userReservations = JSON.parse(localStorage.getItem('userReservations') || '[]');
|
||
const updatedReservations = userReservations.filter(id => id !== currentEventId);
|
||
localStorage.setItem('userReservations', JSON.stringify(updatedReservations));
|
||
|
||
// Remove from calendar
|
||
const event = calendar.getEventById(currentEventId);
|
||
if (event) {
|
||
event.remove();
|
||
}
|
||
|
||
// Close modal and show success message
|
||
document.getElementById('eventModal').style.display = 'none';
|
||
showMessage('Rezervace byla úspěšně zrušena', 'success');
|
||
|
||
// Update the reservations list
|
||
updateReservationsList();
|
||
} catch (error) {
|
||
console.error('Error deleting reservation:', error);
|
||
showMessage('Nepodařilo se zrušit rezervaci', 'error');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Update form submission to track user's reservations
|
||
reservationForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
// ... existing validation code ...
|
||
|
||
try {
|
||
const response = await fetch('/api/reservations', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(reservationData)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// Store reservation ID in localStorage
|
||
const userReservations = JSON.parse(localStorage.getItem('userReservations') || '[]');
|
||
userReservations.push(result.id);
|
||
localStorage.setItem('userReservations', JSON.stringify(userReservations));
|
||
|
||
// Add the new event to the calendar
|
||
calendar.addEvent({
|
||
id: result.id,
|
||
title: `${result.vehicle} - ${result.driverName}`,
|
||
start: `${result.startDate}T${result.startTime}`,
|
||
end: `${result.endDate}T${result.endTime}`,
|
||
extendedProps: {
|
||
driverName: result.driverName,
|
||
vehicle: result.vehicle,
|
||
purpose: result.purpose
|
||
}
|
||
});
|
||
|
||
// Reset form and close modal
|
||
reservationForm.reset();
|
||
document.getElementById('reservationModal').style.display = 'none';
|
||
showMessage('Rezervace byla úspěšně vytvořena', 'success');
|
||
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
showMessage('Nepodařilo se vytvořit rezervaci', 'error');
|
||
}
|
||
});
|
||
|
||
// Make calendar events clickable
|
||
calendar.on('eventClick', function(info) {
|
||
showEventModal(info.event);
|
||
});
|
||
|
||
// Make reservation list items clickable
|
||
document.getElementById('reservationsList').addEventListener('click', function(e) {
|
||
const reservationItem = e.target.closest('.reservation-item');
|
||
if (reservationItem) {
|
||
const eventId = reservationItem.dataset.eventId;
|
||
const event = calendar.getEventById(eventId);
|
||
if (event) {
|
||
showEventModal(event);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |