diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 9192726860fe4f282afe0ba990455ac47b962b6e..a26c015a3c3f3515e868a9d96077105728df5688 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -1,11 +1,10 @@ -from flask import render_template, redirect, url_for, request, jsonify +from flask import render_template, redirect, url_for, request, jsonify, session from app.bookings import bp from app.models import Listings from app import db from app.logger import error_logger -from app.main.utils import calculate_discount +from app.main.utils import calculate_discount, pretty_time import json -import datetime @bp.route('/') @@ -55,9 +54,21 @@ def listings(): ) -@bp.route('/listing/<int:id>') -def show_listing(id): - return render_template('bookings/listings.html', id=1) +@bp.route('/show_listing/<int:id>', methods=['GET']) +def show_listing_dirty(id): + # Retrieve query parameters + session['filter_data'] = request.args.to_dict() + + return redirect(url_for('bookings.listing', id=id)) + +# This route should be used after show_listing if used internally as this clears the ajax parameters before redirecting the user +@bp.route('/listing/<int:id>', methods=['GET']) +def listing(id): + listing = Listings.search_listing(id) + listing.depart_time = pretty_time(listing.depart_time) + listing.destination_time = pretty_time(listing.destination_time) + filter_data = session.pop('filter_data', None) + return render_template('bookings/listing.html', listing=listing, filter_data=filter_data) @bp.route('/filter_bookings', methods=['POST']) def filter_bookings(): diff --git a/app/main/utils.py b/app/main/utils.py index 6af49d8beb385fa1c24b7474e135304d0dbea527..fcaaffff9734bcb7a5831c3d3b688b9f119b2884 100644 --- a/app/main/utils.py +++ b/app/main/utils.py @@ -2,6 +2,7 @@ from flask import current_app from datetime import time, datetime +from datetime import datetime def allowed_image_files(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS'] @@ -27,4 +28,17 @@ def calculate_discount(date): elif 45 <= days_away <= 59: return 10, days_away else: - return 0, days_away \ No newline at end of file + return 0, days_away + + +def pretty_time(unformatted_time, to_12_hour=True): + if not isinstance(unformatted_time, (datetime, time)): + unformatted_time = datetime.strptime(unformatted_time, "%H:%M:%S") + + # Format the time + if to_12_hour: + formatted_time = unformatted_time.strftime("%I:%M %p") + else: + formatted_time = unformatted_time.strftime("%H:%M") + + return formatted_time diff --git a/app/profile/routes.py b/app/profile/routes.py index 0c534e71cd306c60b8c4485569468414773c0eed..6af9ed173137567d218401ed891b25d9b5f4c128 100644 --- a/app/profile/routes.py +++ b/app/profile/routes.py @@ -8,6 +8,11 @@ from app.models import User from app.logger import auth_logger from app import db +@bp.route('/signup/booking/on_hold') +def signup_book_cache(): + session['booking_cache'] = + + @bp.route('/signup', methods=['GET', 'POST']) def signup(): if request.method == 'POST': diff --git a/app/templates/bookings/listing.html b/app/templates/bookings/listing.html new file mode 100644 index 0000000000000000000000000000000000000000..412fcc746ff5eaba5b47a10ee45259dcc0d2875a --- /dev/null +++ b/app/templates/bookings/listing.html @@ -0,0 +1,135 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container mt-5"> + <div id="discountBanner" class="alert alert-success table" role="alert" style="width:90%; display: none;"> + Special Offer! Get <span id="discountPercent"></span>% off on your booking as you are booking <span id="daysAway"></span> days away! + </div> + <div class="date-container"> + <div class="col-md-6 text-start mb-3 d-flex align-items-center flex-md-nowrap flex-wrap" id="dateContainer"> + <label class="form-label me-2">Departure Date:</label> + <div class="col-md-6"> + <input type="date" class="form-control" id="departDate" name="departDate" required> + </div> + <button type="button" class="btn btn-warning ms-2" id="resetDate"> + <i class="fa-solid fa-rotate-right"></i> Reset Date + </button> + </div> + <div class="text-end mb-3"> + <button type="button" class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#filterModal"> + <i class="fa-solid fa-filter"></i> Filter + </button> + </div> + </div> + <div class="row"> + <div class="col-md-6"> + <div class="card mb-4"> + <div class="card-body"> + <h2 class="card-title">Departure Information</h2> + <p><strong>Location:</strong> {{ listing.depart_location }}</p> + <p><strong>Time:</strong> {{ listing.depart_time }}</p> + </div> + </div> + </div> + <div class="col-md-6"> + <div class="card mb-4"> + <div class="card-body"> + <h2 class="card-title">Destination Information</h2> + <p><strong>Location:</strong> {{ listing.destination_location }}</p> + <p><strong>Time:</strong> {{ listing.destination_time }}</p> + </div> + </div> + </div> + </div> + <div class="card mb-4"> + <div class="card-body"> + <h2 class="card-title">Transport Information</h2> + <p><strong>Transport Type:</strong> {{ listing.transport_type }}</p> + <p><strong>Price:</strong> £{{ listing.FairCost }}</p> + </div> + </div> + <div class="text-center"> + <button class="btn btn-secondary btn-lg" id="bookButton" {% if not current_user.is_authenticated %}disabled{% endif %}> + Book Now + </button> + {% if not current_user.is_authenticated %} + <div class="alert alert-warning mt-3" role="alert"> + You must have an account in order to book. + <div class="mt-2"> + <a href="{{ url_for('profile.login_book_cache') }}" class="btn btn-primary me-2">Login</a> + <a href="{{ url_for('profile.signup_book_cache') }}" class="btn btn-warning">Sign Up</a> + </div> + </div> + {% endif %} + </div> + + +</div> +<script> + $(document).ready(function () { + const filterDataDepartDate = "{{ filter_data.date if filter_data else '' }}"; + const resetDateButton = document.getElementById('resetDate'); + + const today = new Date().toISOString().split('T')[0]; + const departDateInput = document.getElementById('departDate'); + + if (!filterDataDepartDate) { + departDateInput.value = today; + } else { + departDateInput.value = filterDataDepartDate; + } + + + departDateInput.setAttribute('min', today); + let maxDate = new Date(); + maxDate.setDate(maxDate.getDate() + 90); + departDateInput.setAttribute('max', maxDate.toISOString().split('T')[0]); + + // Reset date to today when the reset button is clicked + resetDateButton.addEventListener('click', () => { + departDateInput.value = today; + }); + + // Open date picker when the date field is clicked + departDateInput.addEventListener('focus', (event) => { + event.preventDefault(); + departDateInput.showPicker(); + }); + + // Prevent any dates being changed by the user manually typing + departDateInput.addEventListener('keydown', (event) => { + event.preventDefault(); + }); + + // Send the updated date to the server to check for discounts + departDateInput.addEventListener('change', (event) => { + const newDate = event.target.value; + fetch('/bookings/check_date', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ date: newDate }) + }) + .then(response => response.json()) + .then(data => { + if (data.discount) { + const discountBanner = document.getElementById('discountBanner'); + const discountPercent = document.getElementById('discountPercent'); + const daysAway = document.getElementById('daysAway'); + + discountPercent.textContent = data.discount; + daysAway.textContent = data.days_away; + + discountBanner.style.display = 'block'; + } else { + const discountBanner = document.getElementById('discountBanner'); + discountBanner.style.display = 'none'; + } + }) + .catch(error => { + console.error('Error:', error); + }); + }); +}); +</script> +{% endblock %} diff --git a/app/templates/bookings/listings.html b/app/templates/bookings/listings.html index 15c283f161ab0fa48ced001bc8db6d94e09d0393..18d07c0a146cf0dfe545960530eeffb6776f8559 100644 --- a/app/templates/bookings/listings.html +++ b/app/templates/bookings/listings.html @@ -17,7 +17,7 @@ </button> </div> </div> - <!-- This div will be targeted for updating the filtered results --> + <!-- Div is dynamically changed through the /filters route --> <div id="filteredResults"> {% include '_results.html' %} </div> @@ -195,47 +195,6 @@ // Event listener for date picker change $('#departDate').change(applyFilters); - // Common method to apply filters - function applyFilters() { - const filterData = $('#filter-form').serializeArray().reduce((acc, field) => { - acc[field.name] = field.value; - return acc; - }, {}); - - // Convert multi-select to proper array format - filterData.depart_location = $('#depart_location').val(); - filterData.destination_location = $('#destination_location').val(); - - //Get depart date from departure date field and inject into filtered data - const selectedDate = $('#departDate').val(); - filterData.date = selectedDate; - - // Get the current page number from the URL - const urlParams = new URLSearchParams(window.location.search); - const currentPage = urlParams.get('page') || 1; - filterData.page = currentPage; - - $.ajax({ - url: "{{ url_for('bookings.filter_bookings') }}", - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(filterData), - success: function (response) { - $('#filteredResults').html(response.html); // Updates filtered results - $('#filterModal').modal('hide'); - // Hide pagination if filters are applied - if (filterData.depart_location.length > 0 || filterData.destination_location.length > 0 || filterData.min_fair_cost || filterData.max_fair_cost) { - $('.pagination').hide(); - } else { - $('.pagination').show(); - } - }, - error: function () { - alert('Error applying filters. Please try again'); - } - }); - } - // Event listener for applying filters $('#apply-filters').click(applyFilters); @@ -251,21 +210,68 @@ }); }); - // Function to validate the selected date - function validateDate() { - const departDate = document.getElementById('departDate'); - const today = new Date().toISOString().split('T')[0]; - const maxDate = new Date(); - maxDate.setDate(maxDate.getDate() + 90); - const selectedDate = new Date(departDate.value); - - // Check if the selected date is within 90 days in the future AND not in the past - if (selectedDate < new Date(today) || selectedDate > maxDate) { - alert('Please select a date within 90 days from today.'); - departDate.value = maxDate.toISOString().split('T')[0]; - return false; - } - return true; + function handleClick(event, id) { + if (!event.target.classList.contains('main-image')) { + const filterData = getFilterData(); + + // Convert filterData to a URL-encoded query string + const queryString = Object.keys(filterData).map(key => { + return encodeURIComponent(key) + '=' + encodeURIComponent(filterData[key]); + }).join('&'); + + // Construct the target URL with the query string + const targetUrl = "/bookings/show_listing/" + id + "?" + queryString; + + // Redirect to the target URL with the query string + window.location.href = targetUrl; + } +} + + + function getFilterData() { + const filterData = $('#filter-form').serializeArray().reduce((acc, field) => { + acc[field.name] = field.value; + return acc; + }, {}); + + // Convert multi-select to proper array format + filterData.depart_location = $('#depart_location').val(); + filterData.destination_location = $('#destination_location').val(); + + //Get depart date from departure date field and inject into filtered data + const selectedDate = $('#departDate').val(); + filterData.date = selectedDate; + + return filterData; + } + + function applyFilters() { + filterData = getFilterData(); + + // Get the current page number from the URL + const urlParams = new URLSearchParams(window.location.search); + const currentPage = urlParams.get('page') || 1; + filterData.page = currentPage; + + $.ajax({ + url: "{{ url_for('bookings.filter_bookings') }}", + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(filterData), + success: function (response) { + $('#filteredResults').html(response.html); // Updates filtered results + $('#filterModal').modal('hide'); + // Hide pagination if filters are applied + if (filterData.depart_location.length > 0 || filterData.destination_location.length > 0 || filterData.min_fair_cost || filterData.max_fair_cost) { + $('.pagination').hide(); + } else { + $('.pagination').show(); + } + }, + error: function () { + alert('Error applying filters. Please try again'); + } + }); } // Shows pop-up with images attached to specific booking