From 6ebccfde9860fd08f9ec3c75aead69f0b31d3092 Mon Sep 17 00:00:00 2001 From: Ethan-clay03 <ethanclay2017@gmail.com> Date: Thu, 6 Feb 2025 17:17:59 +0000 Subject: [PATCH] Add calculate discount, add banner and strike out to discounted rates, TO DO, add sold out depending on seat availibility --- app/bookings/routes.py | 60 ++++++--- app/main/routes.py | 8 +- app/main/utils.py | 20 ++- app/models/listings.py | 1 + app/templates/_results.html | 20 ++- app/templates/base.html | 2 +- app/templates/bookings/listings.html | 177 ++++++++++++--------------- app/templates/index.html | 36 +++++- 8 files changed, 188 insertions(+), 136 deletions(-) diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 417a3a6..9192726 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -3,7 +3,9 @@ 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 import json +import datetime @bp.route('/') @@ -13,33 +15,51 @@ def redirect_index(): @bp.route('/listings') def listings(): + depart_location = request.args.get('departLocation') + destination_location = request.args.get('destinationLocation') + depart_date = request.args.get('departDate') + page = request.args.get('page', 1, type=int) - locations = Listings.get_all_locations(True) - per_page = 10 # Define how many items per page + per_page = 10 + + # Calculate discount based on departure date + discount, days_away = calculate_discount(depart_date) if depart_date else (0, 0) - # Assuming get_all_listings returns a list, manually paginate all_listings = Listings.get_all_listings() - total_items = len(all_listings) + if depart_location: + all_listings = [listing for listing in all_listings if listing.depart_location == depart_location] + if destination_location: + all_listings = [listing for listing in all_listings if listing.destination_location == destination_location] + + # Calculate pagination items and how many listings exist + total_items = len(all_listings) paginated_listings = all_listings[(page - 1) * per_page: page * per_page] - # Process images process_images(paginated_listings) - return render_template('bookings/listings.html', - items=paginated_listings, - page=page, - total_pages=(total_items + per_page - 1) // per_page, - locations=locations) - + # Get all locations for dropdowns + locations = Listings.get_all_locations(True) + return render_template('bookings/listings.html', + items=paginated_listings, + page=page, + total_pages=(total_items + per_page - 1) // per_page, + locations=locations, + discount=discount, + days_away=days_away, + form_data={ + 'departLocation': depart_location, + 'destinationLocation': destination_location + } + ) @bp.route('/listing/<int:id>') def show_listing(id): return render_template('bookings/listings.html', id=1) -@bp.route('/filter', methods=['POST']) +@bp.route('/filter_bookings', methods=['POST']) def filter_bookings(): try: # Get filter criteria from the request @@ -48,10 +68,12 @@ 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') + depart_date = data.get('date') page = int(data.get('page', 1)) # Get the page parameter or default to 1 - per_page = 10 # Define how many items per page + per_page = 10 # How many listings show per page + + discount, days_away = calculate_discount(depart_date) - # Construct the query query = db.session.query(Listings) if depart_location: @@ -68,9 +90,9 @@ def filter_bookings(): # 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 + paginated_items = filtered_items + page = 1 + total_pages = 1 else: # Paginate the results paginated_items = filtered_items[(page - 1) * per_page: page * per_page] @@ -80,7 +102,7 @@ def filter_bookings(): process_images(paginated_items) # Render only the relevant portion of the results - results_html = render_template('_results.html', items=paginated_items, page=page, total_pages=total_pages) + results_html = render_template('_results.html', items=paginated_items, page=page, total_pages=total_pages, discount=discount, days_away=days_away) return jsonify({'html': results_html}) except Exception as e: @@ -88,7 +110,6 @@ def filter_bookings(): 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) @@ -100,4 +121,3 @@ 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/main/routes.py b/app/main/routes.py index a46cc93..14ec0e8 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -7,19 +7,17 @@ import os @bp.route('/') def index(): - date=datetime.datetime.now() listing_ids = [] - tomorrow_object = date + datetime.timedelta(days=1) top_listings = Listings.get_top_listings(5) + locations = Listings.get_all_locations(True) for listing in top_listings: listing_ids.append(listing.id) top_listing_images = ListingImages.get_selected_main_images(listing_ids) return render_template( - 'index.html', - today = date.strftime('%Y-%m-%d'), - tomorrow = tomorrow_object.strftime('%Y-%m-%d'), + 'index.html', + locations=locations, top_listings=top_listings, top_listing_images=top_listing_images ) diff --git a/app/main/utils.py b/app/main/utils.py index 8ae4ed2..6af49d8 100644 --- a/app/main/utils.py +++ b/app/main/utils.py @@ -1,8 +1,7 @@ # utils.py -import os from flask import current_app -from datetime import time +from datetime import time, datetime def allowed_image_files(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS'] @@ -13,4 +12,19 @@ def generate_time_options(): for minute in range(0, 60, 5): formatted_time = time(hour, minute).strftime('%H:%M') time_options.append(formatted_time) - return time_options \ No newline at end of file + return time_options + + +def calculate_discount(date): + depart_date = datetime.strptime(date, '%Y-%m-%d') + today = datetime.now() + days_away = (depart_date - today).days + + if 80 <= days_away <= 90: + return 25, days_away + elif 60 <= days_away <= 79: + return 15, days_away + elif 45 <= days_away <= 59: + return 10, days_away + else: + return 0, days_away \ No newline at end of file diff --git a/app/models/listings.py b/app/models/listings.py index 4a5348d..f23d6f3 100644 --- a/app/models/listings.py +++ b/app/models/listings.py @@ -65,3 +65,4 @@ class Listings(db.Model): return False return cls.query.get(listing_id) + diff --git a/app/templates/_results.html b/app/templates/_results.html index 51f2716..1a00699 100644 --- a/app/templates/_results.html +++ b/app/templates/_results.html @@ -1,10 +1,15 @@ <div id="filteredResults"> + {% if discount > 0 %} + <div class="alert alert-success table" role="alert" style="width:90%;"> + Special Offer! Get {{ discount }}% off on your booking as you are booking {{days_away}} days away! + </div> + {% endif %} <table class="table table-hover"> <thead> <tr> <th>Main Image</th> <th>Depart Location</th> - <th>Price</th> + <th>Price (£)</th> <th>Destination Location</th> <th>Arrival Time</th> </tr> @@ -14,13 +19,18 @@ <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> + {% if discount > 0 %} + <span style="text-decoration: line-through;">£ {{ item.fair_cost }}</span> + <span>£ {{ item.fair_cost * (1 - discount / 100) }}</span> + {% else %} + £ {{ item.fair_cost }} + {% endif %} + </td> <td>{{ item.destination_location }}</td> <td>{{ item.destination_time }}</td> </tr> {% endfor %} </tbody> </table> - - -</div> +</div> \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 051b0ea..5e00943 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -43,7 +43,7 @@ <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> diff --git a/app/templates/bookings/listings.html b/app/templates/bookings/listings.html index 4e3f768..15c283f 100644 --- a/app/templates/bookings/listings.html +++ b/app/templates/bookings/listings.html @@ -3,43 +3,23 @@ <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"> + <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> <!-- 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> + {% include '_results.html' %} </div> <!-- Pagination Controls --> <nav aria-label="Page navigation"> @@ -142,52 +122,33 @@ </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> +.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; +} + +.table, +.results-container { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +</style> <script> $(document).ready(function () { $('.select2-multiple').select2({ @@ -202,13 +163,37 @@ $('#destination_location').append(new Option(location, location)); }); - $('#datetimepicker1').datetimepicker({ - format: 'DD-MM-YYYY' + const departDateInput = document.getElementById('departDate'); + const resetDateButton = document.getElementById('resetDate'); + + // 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(); }); // 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]); + const today = new Date().toISOString().split('T')[0]; + departDateInput.value = today; + departDateInput.setAttribute('min', today); + + // Set max date to 90 days from 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; + }); + + // Event listener for date picker change + $('#departDate').change(applyFilters); // Common method to apply filters function applyFilters() { @@ -221,6 +206,7 @@ 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; @@ -260,20 +246,28 @@ applyFilters(); // Re-show all listings }); - // 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(); - }); - $('#imageModal .btn-close, #imageModal .btn-secondary').on('click', function() { $('#imageModal').modal('hide'); }); }); + // 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; + } + // Shows pop-up with images attached to specific booking function showModal(imageUrls) { var carouselInner = document.getElementById('carouselInner'); @@ -306,17 +300,6 @@ var imageModal = new bootstrap.Modal(document.getElementById('imageModal')); imageModal.show(); } - - // 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> diff --git a/app/templates/index.html b/app/templates/index.html index 9060f70..8331722 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -13,11 +13,12 @@ </style> </head> <div class="container mt-4"> - <form id="travelForm" class="row g-3"> + <form id="travelForm" class="row g-3" action="{{ url_for('bookings.listings') }}" method="GET"> <div class="col-md-6"> <label for="departLocation" class="form-label">Departure Location:</label> - <select class="form-select select2-multiple" id="departLocation" name="departLocation" required> + <select class="form-select select2-multiple" id="departLocation" name="departLocation"> <option value="" disabled selected>Select locations</option> + <!-- Populate options dynamically if needed --> </select> </div> <div class="col-md-6"> @@ -26,8 +27,9 @@ </div> <div class="col-md-6"> <label for="destinationLocation" class="form-label">Destination Location:</label> - <select class="form-select select2-multiple" id="destinationLocation" name="destinationLocation" required> + <select class="form-select select2-multiple" id="destinationLocation" name="destinationLocation"> <option value="" disabled selected>Select locations</option> + <!-- Populate options dynamically if needed --> </select> </div> <div class="col-md-6"> @@ -51,7 +53,7 @@ <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> $(document).ready(function () { $('.select2-multiple').select2({ @@ -59,12 +61,36 @@ width: '100%' }); - const locations = ['USA', 'Budapest', 'Location3', 'Location3', 'Location3'].sort(); + const locations = JSON.parse('{{ locations|tojson|safe }}'); locations.forEach(location => { $('#departLocation').append(new Option(location, location)); $('#destinationLocation').append(new Option(location, location)); }); }); + + const departDateInput = document.getElementById('departDate'); + const resetDateButton = document.getElementById('resetDate'); + + // 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(); + }); + + // Set default date to today and prevent selecting past dates + const today = new Date().toISOString().split('T')[0]; + departDateInput.value = today; + departDateInput.setAttribute('min', today); + + // Set max date to 90 days from today + let maxDate = new Date(); + maxDate.setDate(maxDate.getDate() + 90); + departDateInput.setAttribute('max', maxDate.toISOString().split('T')[0]); </script> </div> <div> -- GitLab