diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 31707fa7e69e51cc1cc3cd4d171f39ce5d5d82dc..417a3a6d40ddeca5e2db9dce2cea8489708e1c28 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -2,6 +2,7 @@ from flask import render_template, redirect, url_for, request, jsonify from app.bookings import bp from app.models import Listings from app import db +from app.logger import error_logger import json @@ -38,7 +39,6 @@ def listings(): def show_listing(id): return render_template('bookings/listings.html', id=1) - @bp.route('/filter', methods=['POST']) def filter_bookings(): try: @@ -48,33 +48,47 @@ def filter_bookings(): destination_location = data.get('destination_location', []) min_fair_cost = data.get('min_fair_cost') max_fair_cost = data.get('max_fair_cost') + page = int(data.get('page', 1)) # Get the page parameter or default to 1 + per_page = 10 # Define how many items per page # Construct the query query = db.session.query(Listings) if depart_location: - depart_locations = depart_location.split(',') - query = query.filter(Listings.depart_location.in_(depart_locations)) + query = query.filter(Listings.depart_location.in_(depart_location)) if destination_location: - destination_locations = destination_location.split(',') - query = query.filter(Listings.destination_location.in_(destination_locations)) + query = query.filter(Listings.destination_location.in_(destination_location)) if min_fair_cost: query = query.filter(Listings.fair_cost >= float(min_fair_cost)) if max_fair_cost: query = query.filter(Listings.fair_cost <= float(max_fair_cost)) filtered_items = query.all() + total_items = len(filtered_items) + + # Ignore pagination if any filters are applied + if depart_location or destination_location or min_fair_cost or max_fair_cost: + paginated_items = filtered_items # Ignore pagination + page = 1 # Reset page to 1 + total_pages = 1 # Only one page of results + else: + # Paginate the results + paginated_items = filtered_items[(page - 1) * per_page: page * per_page] + total_pages = (total_items + per_page - 1) // per_page # Process images - process_images(filtered_items) + process_images(paginated_items) # Render only the relevant portion of the results - results_html = render_template('_results.html', items=filtered_items) + results_html = render_template('_results.html', items=paginated_items, page=page, total_pages=total_pages) return jsonify({'html': results_html}) except Exception as e: + error_logger.debug(e) return jsonify({'error': str(e)}), 400 + + def process_images(listings): for item in listings: main_image = next((img for img in item.listing_images if img.main_image), None) @@ -86,3 +100,4 @@ def process_images(listings): item.main_image_url = url_for('main.upload_file', filename='booking_image_not_found.jpg') # Must be a single quote JSON otherwise doesn't work in frontend item.image_urls = json.dumps([url_for('main.upload_file', filename=img.image_location) for img in item.listing_images]).replace('"', '"') + diff --git a/app/templates/_results.html b/app/templates/_results.html index b1aa476545892bee12342a167dfc15a4bfc70210..51f2716b319e12021b6d8a580c1a1d9035c9c5ce 100644 --- a/app/templates/_results.html +++ b/app/templates/_results.html @@ -1,13 +1,26 @@ -{% for item in items %} - <div class="results" onclick="handleClick(event, {{ item.id }})"> - <div> - <img src="{{ item.main_image_url }}" alt="Main Image" class="main-image" onclick="event.stopPropagation(); showModal({{ item.image_urls | safe }});"> - </div> - <div> - <div class="title">{{ item.depart_location }}</div> - <div class="price">{{ item.fair_cost }}</div> - <div class="delivery">{{ item.destination_location }}</div> - <div>Save up to 10% with multi-buy</div> - </div> - </div> -{% endfor %} +<div id="filteredResults"> + <table class="table table-hover"> + <thead> + <tr> + <th>Main Image</th> + <th>Depart Location</th> + <th>Price</th> + <th>Destination Location</th> + <th>Arrival Time</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr class="results" onclick="handleClick(event, {{ item.id }})"> + <td><img src="{{ item.main_image_url }}" class="main-image" alt="Main Image" onclick="event.stopPropagation(); showModal({{ item.image_urls | safe }});"></td> + <td>{{ item.depart_location }}</td> + <td>{{ item.fair_cost }}</td> + <td>{{ item.destination_location }}</td> + <td>{{ item.destination_time }}</td> + </tr> + {% endfor %} + </tbody> + </table> + + +</div> diff --git a/app/templates/base.html b/app/templates/base.html index 5ac5d6c815e6381ef142b004420d13b5c25dac67..051b0eae47e45e8cda272754de814d56ec8dd4cd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,25 +5,48 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="csrf-token" content="{{ csrf_token() }}"> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"> - <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='base.css') }}"> - <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='bootstrap_overrides.css') }}"> - <link rel="stylesheet" href="https://kit.fontawesome.com/11fd621de6.js" crossorigin="anonymous"> - <link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/jquery.dataTables.min.css"> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@1.5.2/dist/select2-bootstrap4.min.css"> - <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" /> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/css/tempusdominus-bootstrap-4.min.css" /> + + <!-- jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> - <script src="https://unpkg.com/@popperjs/core@2/dist/umd/popper.js"></script> + + <!-- Bootstrap CSS and JS --> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> + + <!-- Moment.js --> + <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script> + + <!-- FontAwesome --> + <script src="https://kit.fontawesome.com/11fd621de6.js" crossorigin="anonymous"></script> + + <!-- DataTables --> + <link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/jquery.dataTables.min.css"> <script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script> + + <!-- Select2 --> + <link href="https://cdn.jsdelivr.net/npm/@ttskch/select2-bootstrap4-theme@1.5.2/dist/select2-bootstrap4.min.css" rel="stylesheet"> + <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script> + + <!-- Tempus Dominus --> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/css/tempusdominus-bootstrap-4.min.css"> + <script src="https://cdnjs.cloudflare.com/ajax/libs/tempusdominus-bootstrap-4/5.39.0/js/tempusdominus-bootstrap-4.min.js"></script> + + <!-- Popper.js --> + <script src="https://unpkg.com/@popperjs/core@2/dist/umd/popper.js"></script> + + <!-- Timepicker --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/timepicker@1.13.18/jquery.timepicker.min.css"> <script src="https://cdn.jsdelivr.net/npm/timepicker@1.13.18/jquery.timepicker.min.js"></script> - <link rel="stylesheet" href="{{ url_for('static', filename='generic.js') }}"> + + <!-- Custom CSS and JS --> + <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='base.css') }}"> + <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='bootstrap_overrides.css') }}"> + <script src="{{ url_for('static', filename='generic.js') }}"></script> + <title>Horizon Travels</title> </head> + <body> <div class="main_content"> <div class="wrapper"> diff --git a/app/templates/bookings/listings.html b/app/templates/bookings/listings.html index 6341d44528086a34c00a0a803567dcf1d73c6772..4e3f768a06d1903b632dd3469cda5c14cb739447 100644 --- a/app/templates/bookings/listings.html +++ b/app/templates/bookings/listings.html @@ -1,76 +1,49 @@ {% extends 'base.html' %} {% block content %} -<style> -.select2-container { - width: 100% !important; -} - - -.results-container { - width: 60%; - margin: 0 auto; - font-family: Arial, sans-serif; -} - -.filter-button-container { - margin-top: 20px; - margin-bottom: 20px; - text-align: right; -} - -.results { - border: 1px solid #ccc; - padding: 10px; - margin-bottom: 20px; - display: flex; - gap: 20px; - cursor: pointer; -} - -.main-image { - max-width: 150px; - cursor: pointer; -} - -.modal-footer { - display: flex; - justify-content: space-between; -} - -.form-control { - width: 100%; -} - -</style> -<head> - <script src="{{ url_for('static', filename='generic.js') }}"></script> -</head> -<div class="filter-button-container"> - <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 class="results-container"> - <div id="resultsContainer"> - {% for item in items %} - <div class="results" onclick="handleClick(event, {{ item.id }})"> - <div> - <img src="{{ item.main_image_url }}" alt="Main Image" class="main-image" onclick="event.stopPropagation(); showModal({{ item.image_urls | safe }});"> - </div> - <div> - <div class="title">{{ item.depart_location }}</div> - <div class="price">{{ item.fair_cost }}</div> - <div class="delivery">{{ item.destination_location }}</div> - <div>Save up to 10% with multi-buy</div> - </div> +<div class="my_container my-4"> + <div class="results-container"> + <div class="col-md-6 text-start mb-3 d-flex align-items-center flex-md-nowrap flex-wrap" id="dateContainer"> + <label for="departureDate" class="form-label me-2">Departure Date:</label> + <input type="date" class="form-control me-2 flex-grow-1" id="departDate" name="departDate" required> + <button type="button" class="btn btn-warning" 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> - {% endfor %} </div> - + <!-- This div will be targeted for updating the filtered results --> + <div id="filteredResults"> + <table class="table table-hover"> + <thead> + <tr> + <th>Main Image</th> + <th>Depart Location</th> + <th>Price</th> + <th>Destination Location</th> + <th>Arrival Time</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr class="results" onclick="handleClick(event, {{ item.id }})"> + <td><img src="{{ item.main_image_url }}" class="main-image" alt="Main Image" onclick="event.stopPropagation(); showModal({{ item.image_urls | safe }});"></td> + <td>{{ item.depart_location }}</td> + <td>{{ item.fair_cost }}</td> + <td>{{ item.destination_location }}</td> + <td>{{ item.destination_time }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> <!-- Pagination Controls --> <nav aria-label="Page navigation"> - <ul class="pagination"> + <ul class="pagination justify-content-center"> {% if page > 1 %} <li class="page-item"> <a class="page-link" href="{{ url_for('bookings.listings', page=page - 1) }}" aria-label="Previous"> @@ -124,9 +97,10 @@ </div> </div> </div> -<!-- Hidden Filter Modal --> + +<!-- Filter Modal --> <div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> - <div class="modal-dialog"> + <div class="modal-dialog modal-dialog-centered modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="filterModalLabel"><i class="fa-solid fa-filter"></i> Filter Options</h5> @@ -134,15 +108,13 @@ </div> <div class="modal-body"> <form id="filter-form"> - <div class="form-group"> + <div class="mb-3"> <label for="depart_location" class="form-label">Depart Location:</label> - <select class="form-control select2-multiple" id="depart_location" name="depart_location[]" multiple="multiple"> - </select> + <select class="form-control select2-multiple" id="depart_location" name="depart_location[]" multiple="multiple"></select> </div> - <div class="form-group"> + <div class="mb-3"> <label for="destination_location" class="form-label">Destination Location:</label> - <select class="form-control select2-multiple" id="destination_location" name="destination_location[]" multiple="multiple"> - </select> + <select class="form-control select2-multiple" id="destination_location" name="destination_location[]" multiple="multiple"></select> </div> <div class="form-group"> <label for="min_fair_cost" class="form-label">Minimum Fair Cost:</label> @@ -155,7 +127,7 @@ <div class="form-group"> <label for="transport_type" class="form-label">Transport Type:</label> <input type="text" class="form-control" id="transport_type" name="transport_type" value="Airplane" disabled> - </div> + </div> </form> </div> <div class="modal-footer"> @@ -169,6 +141,53 @@ </div> </div> +<style> + @media (max-width: 768px) { + #dateContainer { + flex-direction: column; + align-items: flex-start; + } + #dateContainer .form-label, + #dateContainer .form-control, + #dateContainer .btn { + width: 100%; + margin-bottom: 10px; + } + } + + .results { + cursor: pointer; + transition: transform 0.3s; + } + + .results:hover { + transform: scale(1.05); + } + + .main-image { + max-width: 150px; + height: auto; + } + + .modal-footer { + display: flex; + justify-content: space-between; + } + + .datepicker { + position: relative; + } + + .datepicker .input-group-append { + cursor: pointer; + } + + .results-container { + width: 90%; + margin-left: auto; + margin-right: auto; + } + </style> <script> $(document).ready(function () { $('.select2-multiple').select2({ @@ -177,64 +196,79 @@ width: '100%' }); - const locations = JSON.parse('{{ locations|tojson|safe }}'); locations.forEach(location => { $('#depart_location').append(new Option(location, location)); $('#destination_location').append(new Option(location, location)); }); + $('#datetimepicker1').datetimepicker({ + format: 'DD-MM-YYYY' + }); + + // Set default date to today and prevent selecting past dates + document.getElementById('departDate').value = new Date().toISOString().split('T')[0]; + document.getElementById('departDate').setAttribute('min', new Date().toISOString().split('T')[0]); - // Apply filters from the modal - $('#apply-filters').click(function () { + // Common method to apply filters + function applyFilters() { const filterData = $('#filter-form').serializeArray().reduce((acc, field) => { acc[field.name] = field.value; return acc; }, {}); - //Get bookings which match the filter results user inputs + // Convert multi-select to proper array format + filterData.depart_location = $('#depart_location').val(); + filterData.destination_location = $('#destination_location').val(); + + 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) { - $('#resultsContainer').html(response.html); + $('#filteredResults').html(response.html); // Updates filtered results $('#filterModal').modal('hide'); - $('.pagination').hide(); // Hide pagination as causes it to reset when changing page + // 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'); } }); - }); + } + - // Reset filters + // Event listener for applying filters + $('#apply-filters').click(applyFilters); + + // Event listener for resetting filters $('#reset-filters').click(function () { $('#filter-form')[0].reset(); // Reset filter modal - $('.pagination').show(); // Re-show pagination - - $.ajax({ - url: "{{ url_for('bookings.filter_bookings') }}", - type: 'POST', - contentType: 'application/json', - data: JSON.stringify({}), // Send empty filter data to re-show all listings - success: function (response) { - $('#resultsContainer').html(response.html); - $('#filterModal').modal('hide'); - }, - error: function () { - alert('Error resetting filters.'); - } - }); + applyFilters(); // Re-show all listings }); - $('.select2-multiple').select2({ - width: 'resolve', - placeholder: 'Select an option' + // Event listener for date picker change + $('#departDate').change(applyFilters); + + // Reset date functionality + document.getElementById('resetDate').addEventListener('click', function() { + document.getElementById('departDate').value = new Date().toISOString().split('T')[0]; + applyFilters(); }); - // Close image modal $('#imageModal .btn-close, #imageModal .btn-secondary').on('click', function() { $('#imageModal').modal('hide'); }); @@ -242,8 +276,6 @@ // Shows pop-up with images attached to specific booking function showModal(imageUrls) { - console.log("showModal called with imageUrls:", imageUrls); - var carouselInner = document.getElementById('carouselInner'); var carouselIndicators = document.getElementById('carouselIndicators'); carouselInner.innerHTML = ''; @@ -275,7 +307,18 @@ imageModal.show(); } -</script> + // Set date field and attach reset button + document.getElementById('resetDate').addEventListener('click', function() { + document.getElementById('departDate').value = new Date().toISOString().split('T')[0]; + }); + + // Prevent selecting past dates + document.getElementById('departDate').setAttribute('min', new Date().toISOString().split('T')[0]); + + // Set default date to today + document.getElementById('departDate').value = new Date().toISOString().split('T')[0]; +</script> + </body> </html> {% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index ea2f31cc331349fb2649abdfe972a56e18e26439..9060f7052499188546b7ee5ef876d459574c073e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -11,8 +11,6 @@ width: 100%; } </style> - <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet"> - <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js" defer></script> </head> <div class="container mt-4"> <form id="travelForm" class="row g-3"> @@ -50,36 +48,11 @@ <option value="firstClass">First Class</option> </select> </div> - <div class="col-md-6"> - <label for="tripType" class="form-label">Trip Type:</label> - <select id="tripType" class="form-select" name="tripType" onchange="toggleReturnFields()"> - <option value="oneWay">One-way</option> - <option value="return">Return</option> - </select> - </div> - <div class="col-md-6"> - <label for="returnDate" class="form-label">Return Date:</label> - <input type="date" class="form-control" id="returnDate" name="returnDate" disabled> - </div> - <div class="col-12"> + <div class="col-md-6 d-flex align-items-end justify-content-left"> <button type="submit" class="btn btn-primary">Search</button> </div> - </form> - + </form> <script> - function toggleReturnFields() { - var tripType = document.getElementById("tripType").value; - var returnDate = document.getElementById("returnDate"); - var returnTime = document.getElementById("returnTime"); - if (tripType === "return") { - returnDate.disabled = false; - returnTime.disabled = false; - } else { - returnDate.disabled = true; - returnTime.disabled = true; - } - } - $(document).ready(function () { $('.select2-multiple').select2({ placeholder: "Select locations",