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('"', '&quot;')
-
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 (&pound;)</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;">&pound; {{ item.fair_cost }}</span>
+                    <span>&pound; {{ item.fair_cost * (1 - discount / 100) }}</span>
+                    {% else %}
+                    &pound; {{ 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