diff --git a/app/admin/routes.py b/app/admin/routes.py index a82d89861a678e13ca769a3b2ea30916cd616b61..100d1127cb67d3867baf9b9f974931120ce9a970 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -18,7 +18,15 @@ def home(): @bp.route('/manage_bookings') @permission_required(admin_permission) def manage_bookings(): - return render_template('admin/manage_bookings.html') + locations = Listings.get_all_locations() + return render_template('admin/manage_bookings.html', locations=locations) + +@bp.route('/manage_bookings/edit/<int:id>') +@permission_required(admin_permission) +def edit_booking(id): + locations = Listings.get_all_locations() + listing_information = Listings.search_listing(id) + return render_template('admin/edit_booking.html', locations=locations, listing=listing_information) @bp.route('/manage_users') @permission_required(super_admin_permission) diff --git a/app/bookings/routes.py b/app/bookings/routes.py index c4f664e97c1ea1e55965e920717cbba959c57f72..851c684cc3f7cdad6a180fc1009ffafa1a1988c2 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -20,7 +20,7 @@ def listings(): elif item.listing_images: item.main_image_url = url_for('main.upload_file', filename=item.listing_images[0].image_location) else: - item.main_image_url = "/path/to/default-image.jpg" + 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/models/listings.py b/app/models/listings.py index 08970b165009545983a0b2c07c64b4e3a9877336..b74c85871c46356d191ab26f7738d42f38e9a3bb 100644 --- a/app/models/listings.py +++ b/app/models/listings.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import relationship from sqlalchemy import Time from app import db +from sqlalchemy.sql import text class Listings(db.Model): __tablename__ = 'listings' @@ -18,6 +19,12 @@ class Listings(db.Model): def get_all_listings(cls): return cls.query.all() + @classmethod + def get_all_locations(cls): + query = text("SELECT depart_location AS location FROM listings UNION SELECT destination_location AS location FROM listings") + result = db.session.execute(query) + return [location[0] for location in result] + @classmethod def create_listing(cls, depart_location, depart_time, destination_location, destination_time, fair_cost, transport_type): new_flight = cls(depart_location=depart_location, diff --git a/app/templates/admin/edit_booking.html b/app/templates/admin/edit_booking.html new file mode 100644 index 0000000000000000000000000000000000000000..f51a0c2d02f7dc5f095fb8543b595913407da021 --- /dev/null +++ b/app/templates/admin/edit_booking.html @@ -0,0 +1,141 @@ +{% extends 'base.html' %} +{% macro time_options(selected_time) %} + {% for time in ["00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00", "04:30", + "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", + "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", "14:00", "14:30", + "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", + "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30"] %} + <option value="{{ time }}" {% if selected_time == time %}selected{% endif %}>{{ time }}</option> + {% endfor %} +{% endmacro %} +{% block content %} +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Edit Booking</title> + <link rel="stylesheet" href="path/to/your/css/style.css"> +</head> +<body> + <div class="container mt-4"> + <h2>Edit Booking</h2> + <form id="editBookingForm" class="row g-3" action="{{ url_for('admin.get_bookings', id=listing.id) }}" method="post" enctype="multipart/form-data"> + <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> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}" {% if location == listing.depart_location %}selected{% endif %}>{{ location }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="departTime" class="form-label">Departure Time:</label> + <select class="form-control select2-dropdown" id="departTime" name="departTime" required> + <option value="" disabled selected>Select a time</option> + {{ time_options(listing.depart_time.strftime('%H:%M')) }} + </select> + </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> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}" {% if location == listing.destination_location %}selected{% endif %}>{{ location }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="destinationTime" class="form-label">Arrival Time:</label> + <select class="form-control select2-dropdown" id="destinationTime" name="destinationTime" required> + <option value="" disabled selected>Select a time</option> + {{ time_options(listing.destination_time.strftime('%H:%M')) }} + </select> + </div> + + <script> + $(document).ready(function() { + $('.select2-dropdown').select2({ + placeholder: "Select a time", + width: '100%', + minimumResultsForSearch: Infinity + }); + }); + </script> + + <div class="col-md-6"> + <label for="fairCost" class="form-label">Fair Cost:</label> + <input type="number" step="0.01" class="form-control" id="fairCost" name="fairCost" value="{{ listing.fair_cost }}" required> + </div> + <div class="col-md-6"> + <label for="transportType" class="form-label">Transport Type:</label> + <select id="transportType" class="form-select" name="transportType" value="Airplane" disabled> + <option value="Airplane">Airplane</option> + </select> + </div> + <div class="col-md-12"> + <label for="images" class="form-label">Upload Images:</label> + <input type="file" class="form-control" id="images" name="images" multiple> + <div class="current-images mt-3"> + <h4>Current Images:</h4> + <div class="row"> + {% for image in listing.listing_images %} + <div class="col-md-3 mb-3"> + <div class="card"> + <div class="image-container"> + <img src="{{ url_for('main.upload_file', filename=image.image_location) }}" class="img-thumbnail"> + </div> + <div class="card-body"> + <input type="radio" class="btn-check" name="main_image" id="{{image.id}}" value="{{ image.main_image }}" autocomplete="off" {% if image.main_image == 1 %}checked{% endif %}> + <label class="btn btn-outline-success w-100" id="{{image.id}}">Main Image</label> + + <button type="button" class="btn btn-danger btn-sm mt-2 delete-image-btn w-100">Delete</button> + </div> + </div> + </div> + {% endfor %} + </div> + </div> + </div> + <div class="col-12"> + <button type="submit" class="btn btn-primary">Update Booking</button> + </div> + </form> + </div> + + <script> + // JavaScript to handle image deletion + document.querySelectorAll('.delete-image-btn').forEach(button => { + button.addEventListener('click', function() { + const image = this.getAttribute('data-image'); + this.closest('.col-md-3').remove(); + // Add logic to handle image deletion in the backend + console.log('Deleted image:', image); + }); + }); + </script> +</body> +</html> +<style> + .image-container { + position: relative; + width: 100%; + padding-top: 100%; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + } + + .image-container img { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + object-fit: cover; + transform: translate(-50%, -50%); /* Makes image center */ + } + </style> +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/manage_bookings.html b/app/templates/admin/manage_bookings.html index e3873d661af38a28a7a26c224aea25f5df10831c..88aff7ce090b3d0969600f6380fef1fc66b34583 100644 --- a/app/templates/admin/manage_bookings.html +++ b/app/templates/admin/manage_bookings.html @@ -151,7 +151,8 @@ }); // Populate location options, TO UPDATE WITH LIVE LOCATIONS - const locations = ['USA', 'Budapest', 'Location3'].sort(); + const locations = JSON.parse('{{ locations|tojson|safe }}'); + locations.sort(); locations.forEach(location => { $('#depart_location').append(new Option(location, location)); $('#destination_location').append(new Option(location, location)); @@ -202,7 +203,7 @@ language: { emptyTable: "No bookings could be found with the current filters" }, - createdRow: function(row, data, dataIndex) { //Attach ID to edit/delete buttons for use in modifying individual enteries + createdRow: function(row, data, dataIndex) { //Attach ID to edit button and delete button. $(row).find('.edit-btn').attr('data-id', data.id); $(row).find('.delete-btn').attr('data-id', data.id); } @@ -216,10 +217,10 @@ $('#manage_bookings tbody').on('click', '.edit-btn', function() { const id = $(this).data('id'); - const data = table.row($(this).parents('tr')).data(); - alert('Edit entry for ' + data.depart_location + ' with ID ' + id); + window.location.href = `manage_bookings/edit/${id}`; }); + let delete_booking; $('#manage_bookings tbody').on('click', '.delete-btn', function() { delete_booking = table.row($(this).parents('tr')); @@ -242,7 +243,7 @@ } }); } else { - alert('Please type "CONFIRM" to delete the entry.'); + alert('Please type "CONFIRM" to delete the booking.'); } }); diff --git a/app/uploads/listing_images/booking_image_not_found.jpg b/app/uploads/listing_images/booking_image_not_found.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04ca59b6bc6298687204bb94533f3980394027d9 Binary files /dev/null and b/app/uploads/listing_images/booking_image_not_found.jpg differ