236 lines
9.1 KiB
HTML
236 lines
9.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<h2>Book {{ resource.name }}</h2>
|
|
{% if resource.image_url %}
|
|
<img src="{{ url_for('static', filename=resource.image_url) }}" alt="{{ resource.name }}" class="img-fluid mb-4 rounded" style="max-width: 100%; max-height: 400px; object-fit: cover; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
|
|
{% endif %}
|
|
<p class="text-muted">{{ resource.description }}</p>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div id="calendar"></div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Create Booking</h5>
|
|
<form method="POST" id="bookingForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<div class="mb-3">
|
|
<label for="start_time" class="form-label">Start Time</label>
|
|
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="end_time" class="form-label">End Time</label>
|
|
<input type="datetime-local" class="form-control" id="end_time" name="end_time" required readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="purpose" class="form-label">Purpose</label>
|
|
<textarea class="form-control" id="purpose" name="purpose" rows="3" placeholder="e.g., Client meeting, Team call, etc."></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">Book Now</button>
|
|
<button type="button" class="btn btn-secondary w-100 mt-2" id="clearSelection">Clear Selection</button>
|
|
</form>
|
|
<div class="mt-3">
|
|
<div class="alert alert-info" role="alert">
|
|
<strong>Booking Rules:</strong><br>
|
|
- Bookings can be made up to 6 months in advance<br>
|
|
- Only one booking per resource at a time (no overlapping slots)<br>
|
|
- Click on the calendar to select start time<br>
|
|
- Click again to select end time<br>
|
|
- Green events are your bookings<br>
|
|
- Red events are others' bookings
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.css' rel='stylesheet' />
|
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js'></script>
|
|
|
|
<style>
|
|
#calendar {
|
|
max-width: 100%;
|
|
margin: 0 auto;
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.fc-event {
|
|
cursor: pointer;
|
|
}
|
|
.fc-timegrid-slot {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var calendarEl = document.getElementById('calendar');
|
|
var startTimeInput = document.getElementById('start_time');
|
|
var endTimeInput = document.getElementById('end_time');
|
|
var purposeInput = document.getElementById('purpose');
|
|
var clearButton = document.getElementById('clearSelection');
|
|
var bookingForm = document.getElementById('bookingForm');
|
|
|
|
var selectedStart = null;
|
|
var selectedEnd = null;
|
|
var tempEvent = null;
|
|
|
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
|
initialView: 'timeGridWeek',
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
|
},
|
|
weekends: false,
|
|
slotMinTime: '06:00:00',
|
|
slotMaxTime: '22:00:00',
|
|
allDaySlot: false,
|
|
selectable: true,
|
|
selectMirror: true,
|
|
nowIndicator: true,
|
|
slotDuration: '00:30:00',
|
|
height: 'auto',
|
|
minDate: new Date(), // Cannot book before today
|
|
maxDate: new Date(Date.now() + 182 * 24 * 60 * 60 * 1000), // Cannot book more than 6 months ahead
|
|
events: {
|
|
url: '/api/resource-bookings/{{ resource.id }}',
|
|
failure: function() {
|
|
alert('Failed to load bookings');
|
|
}
|
|
},
|
|
dateClick: function(info) {
|
|
// If we haven't selected start yet, or we're starting over
|
|
if (selectedStart === null) {
|
|
selectedStart = info.date;
|
|
selectedEnd = new Date(info.date.getTime() + 60 * 60 * 1000); // Default 1 hour
|
|
|
|
// Update inputs
|
|
startTimeInput.value = formatDateTimeLocal(selectedStart);
|
|
endTimeInput.value = formatDateTimeLocal(selectedEnd);
|
|
|
|
// Show temporary event
|
|
if (tempEvent) {
|
|
tempEvent.remove();
|
|
}
|
|
tempEvent = calendar.addEvent({
|
|
title: 'New Booking (select end time)',
|
|
start: selectedStart,
|
|
end: selectedEnd,
|
|
color: '#6c757d',
|
|
editable: false
|
|
});
|
|
} else {
|
|
// Selecting end time
|
|
if (info.date > selectedStart) {
|
|
selectedEnd = info.date;
|
|
endTimeInput.value = formatDateTimeLocal(selectedEnd);
|
|
|
|
// Update temporary event
|
|
if (tempEvent) {
|
|
tempEvent.remove();
|
|
}
|
|
tempEvent = calendar.addEvent({
|
|
title: 'New Booking (ready to submit)',
|
|
start: selectedStart,
|
|
end: selectedEnd,
|
|
color: '#0d6efd',
|
|
editable: false
|
|
});
|
|
|
|
// Focus on purpose field
|
|
purposeInput.focus();
|
|
} else {
|
|
alert('End time must be after start time. Please select a later time.');
|
|
}
|
|
}
|
|
},
|
|
select: function(info) {
|
|
selectedStart = info.start;
|
|
selectedEnd = info.end;
|
|
|
|
startTimeInput.value = formatDateTimeLocal(selectedStart);
|
|
endTimeInput.value = formatDateTimeLocal(selectedEnd);
|
|
|
|
if (tempEvent) {
|
|
tempEvent.remove();
|
|
}
|
|
tempEvent = calendar.addEvent({
|
|
title: 'New Booking (ready to submit)',
|
|
start: selectedStart,
|
|
end: selectedEnd,
|
|
color: '#0d6efd',
|
|
editable: false
|
|
});
|
|
|
|
// Set default purpose based on resource type if not already set
|
|
if (!purposeInput.value) {
|
|
purposeInput.value = getDefaultValue(resource.resource_type);
|
|
}
|
|
|
|
purposeInput.focus();
|
|
}
|
|
});
|
|
|
|
calendar.render();
|
|
|
|
// Clear selection button
|
|
clearButton.addEventListener('click', function() {
|
|
selectedStart = null;
|
|
selectedEnd = null;
|
|
startTimeInput.value = '';
|
|
endTimeInput.value = '';
|
|
purposeInput.value = '';
|
|
if (tempEvent) {
|
|
tempEvent.remove();
|
|
tempEvent = null;
|
|
}
|
|
});
|
|
|
|
// Set default purpose on form submit
|
|
bookingForm.addEventListener('submit', function(e) {
|
|
if (!selectedStart || !selectedEnd) {
|
|
e.preventDefault();
|
|
alert('Please select a time slot on the calendar');
|
|
return false;
|
|
}
|
|
|
|
// Set default purpose if empty
|
|
if (!purposeInput.value.trim()) {
|
|
purposeInput.value = getDefaultValue(resource.resource_type);
|
|
}
|
|
});
|
|
|
|
// Get default purpose based on resource type
|
|
function getDefaultValue(resourceType) {
|
|
const purposeMap = {
|
|
'Work Office': 'Office work',
|
|
'Meeting Room': 'Meeting',
|
|
'Phone Booth': 'Phone call',
|
|
'Focus Room': 'Focus work',
|
|
'Collaborative Space': 'Collaboration',
|
|
'Conference Room': 'Conference'
|
|
};
|
|
return purposeMap[resourceType] || '';
|
|
}
|
|
|
|
function formatDateTimeLocal(date) {
|
|
var year = date.getFullYear();
|
|
var month = String(date.getMonth() + 1).padStart(2, '0');
|
|
var day = String(date.getDate()).padStart(2, '0');
|
|
var hours = String(date.getHours()).padStart(2, '0');
|
|
var minutes = String(date.getMinutes()).padStart(2, '0');
|
|
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|