diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 14f6a991d234731d8e29ba59839349c1c72e3a85..21a5c45f38673fd0e680dee80282766c5cdd5f41 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -375,3 +375,41 @@ def generate_ticket(id): pdf = create_plane_ticket(booking, listing) return send_file(pdf, as_attachment=True, download_name='plane_ticket.pdf') + + +@bp.route('/get_user_bookings', methods=['GET']) +@permission_required(user_permission) +def get_user_bookings(): + query = db.session.query(Bookings).join(Listings) + + depart_location = request.args.get('depart_location') + destination_location = request.args.get('destination_location') + booking_date = request.args.get('booking_date') + depart_date = request.args.get('depart_date') + + # Only get non-cancelled bookings + query = query.filter(Bookings.cancelled == 0) + if depart_location: + depart_locations = depart_location.split(',') + query = query.filter(Listings.depart_location.in_(depart_locations)) + if destination_location: + destination_locations = destination_location.split(',') + query = query.filter(Listings.destination_location.in_(destination_locations)) + if booking_date: + query = query.filter(Bookings.booking_date == booking_date) + if depart_date: + query = query.filter(Bookings.depart_date == depart_date) + + filtered_data = query.all() + result = [ + { + 'id': booking.id, + 'depart_location': booking.listing.depart_location, + 'booking_date': booking.booking_date.strftime("%a, %d %b %Y"), + 'destination_location': booking.listing.destination_location, + 'depart_date': booking.depart_date.strftime("%a, %d %b %Y"), + } for booking in filtered_data + ] + + return jsonify(result) + diff --git a/app/models/bookings.py b/app/models/bookings.py index 4881e91ee5f3ab415f6e45d00accd33711a9fa4d..f9834f2f43b6c168d59859cb8ee85a1cbe14acf7 100644 --- a/app/models/bookings.py +++ b/app/models/bookings.py @@ -2,21 +2,23 @@ from app import db from flask_login import UserMixin from app.logger import error_logger from datetime import datetime +from sqlalchemy.orm import relationship class Bookings(UserMixin, db.Model): __tablename__ = 'bookings' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, nullable=False) - listing_id = db.Column(db.Integer, nullable=False) + listing_id = db.Column(db.Integer, db.ForeignKey('listings.id'), nullable=False) amount_paid = db.Column(db.Float, nullable=False) seat_type = db.Column(db.String(50), nullable=False) - num_seats = db.Column(db.Integer, nullable=False) + num_seats = db.Column(db.Integer, nullable=False) cancelled = db.Column(db.Boolean, default=False) cancelled_date = db.Column(db.Date, nullable=False) booking_date = db.Column(db.Date, nullable=False) depart_date = db.Column(db.Date, nullable=False) last_four_card_nums = db.Column(db.String(4), nullable=False) + listing = relationship('Listings', back_populates='bookings') @classmethod def create_booking(cls, listing_id, user_id, amount_paid, seat_type, num_seats, depart_date, last_four_card_nums): @@ -44,3 +46,26 @@ class Bookings(UserMixin, db.Model): @classmethod def search_booking(cls, id): return cls.query.get(id) + + @classmethod + def get_user_bookings(cls, user_id): + try: + return cls.query.filter_by(user_id=user_id).all() + except Exception as e: + error_logger.error(f"Error retrieving bookings for user ID {user_id}: {e}") + return None + + @classmethod + def check_booking_user_ids_match(cls, booking_id, user_id): + booking = cls.query.filter_by(id=booking_id, user_id=user_id).first() + return booking is not None + + @classmethod + def cancel_booking(cls, booking_id): + booking = cls.query.get(booking_id) + if booking: + booking.cancelled = True + booking.cancelled_date = datetime.utcnow().date() + db.session.commit() + return True + return False \ No newline at end of file diff --git a/app/models/listings.py b/app/models/listings.py index 7d44f1a4ab0bdf786ac78ed448930df33581d519..ce3947482e1f1f923f0450998848f7a4ce885205 100644 --- a/app/models/listings.py +++ b/app/models/listings.py @@ -6,15 +6,16 @@ from sqlalchemy.sql import text class Listings(db.Model): __tablename__ = 'listings' - id = db.Column(db.Integer(), nullable=False, primary_key=True) + id = db.Column(db.Integer, primary_key=True) depart_location = db.Column(db.String(255), nullable=False) - depart_time = db.Column(Time, nullable=False) + depart_time = db.Column(db.Time, nullable=False) destination_location = db.Column(db.String(255), nullable=False) - destination_time = db.Column(Time, nullable=False) - economy_fair_cost = db.Column(db.Float(), nullable=False) - business_fair_cost = db.Column(db.Float(), nullable=False) + destination_time = db.Column(db.Time, nullable=False) + economy_fair_cost = db.Column(db.Float, nullable=False) + business_fair_cost = db.Column(db.Float, nullable=False) transport_type = db.Column(db.String(255), nullable=False) listing_images = relationship("ListingImages", back_populates="listing", cascade="all, delete-orphan") + bookings = relationship('Bookings', back_populates='listing', cascade="all, delete-orphan") @classmethod def get_all_listings(cls): diff --git a/app/models/user.py b/app/models/user.py index ae88eb788f1511db5027d84a7b6c6bde3764d882..fd4bc186a17f8a57c656acbbb588023caab08199 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -72,3 +72,16 @@ class User(UserMixin, db.Model): return True return False + + @classmethod + def update_user_details(cls, user_id, username, email): + # Ensure the user exists + user = cls.search_user_id(user_id) + + if user: + user.username = username + user.email = email + db.session.commit() + return True + + return False diff --git a/app/profile/routes.py b/app/profile/routes.py index 8d3a7bd1e0c7d10f0520ae7ca1221873933d3b8d..1f41c8760d9bc70a744d6d2fd489770d05749eaf 100644 --- a/app/profile/routes.py +++ b/app/profile/routes.py @@ -4,7 +4,7 @@ from flask_principal import Identity, identity_changed from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash from app.profile import bp -from app.models import User +from app.models import User, Bookings, Listings from app.logger import auth_logger from app import db, permission_required, user_permission @@ -207,6 +207,10 @@ def password_reset_from_profile(): @bp.route('/password-reset/reset-password', methods=['POST']) def password_reset_process(): email = session.get('password-reset-email') + + if email == None: + email = current_user.email + password1 = request.form.get('password-1') password2 = request.form.get('password-2') @@ -230,8 +234,11 @@ def index(): @bp.route('/manage_bookings') def manage_bookings(): if current_user.is_authenticated: - return render_template('profile/manage_bookings.html', username=current_user.username) + bookings = Bookings.get_user_bookings(current_user.id) + locations = Listings.get_all_locations(True) + return render_template('profile/manage_bookings.html', username=current_user.username, bookings=bookings, locations=locations) + flash('You must be logged in to view your current bookings', 'error') return redirect(url_for('profile.login')) @bp.route('/manage_profile', methods=['GET', 'POST']) @@ -242,8 +249,32 @@ def manage_profile(): flash('You must be logged in to update your profile','error') return redirect(url_for('main.index')) if request.method == 'POST': - name = request.form['name'] + username = request.form['username'] email = request.form['email'] - return redirect(url_for('profile.home')) + User.update_user_details(user.id, username, email) + + flash('Successfully updated profile details', 'success') + return redirect(url_for('profile.manage_profile')) return render_template('profile/manage_profile.html', user=user) + + +@bp.route('/cancel_booking', methods=['POST']) +@permission_required(user_permission) +def cancel_booking(): + data = request.get_json() + booking_id = data.get('booking_id') + if not booking_id: + return jsonify({'error': 'Missing booking_id'}), 400 + + success = Bookings.cancel_booking(booking_id) + if success: + return jsonify({'message': 'Booking cancelled successfully'}), 200 + else: + return jsonify({'error': 'Failed to cancel booking'}), 400 + + +@bp.route('/manage_bookings/view/<int:id>') +def manage_profile_view_booking(id): + + return render_template('profile/view_booking.html') diff --git a/app/templates/profile/manage_bookings.html b/app/templates/profile/manage_bookings.html index e094c38dec5fc9f0d85536fda4fcb0b732e720c2..949b84ac32f4b7b276641e37cb2af327e50abd50 100644 --- a/app/templates/profile/manage_bookings.html +++ b/app/templates/profile/manage_bookings.html @@ -1,7 +1,232 @@ {% extends 'base.html' %} {% block content %} -<div> - <p>Welcome {{username}}, manage bookings here!</p> +<div class="container my-4"> + <div class="d-flex justify-content-between mb-3"> + <h2>Manage Bookings</h2> + <div> + <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="table-container"> + <table id="manage_bookings" class="table table-striped table-bordered display hover" style="width:100%"> + <thead> + <tr> + <th>Booking ID</th> + <th>Booking Date</th> + <th>Departure Date</th> + <th>Departure Location</th> + <th>Destination Location</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for booking in bookings %} + <tr> + <td>{{ booking.id }}</td> + <td>{{ booking.booking_date }}</td> + <td>{{ booking.departure_date }}</td> + <td>{{ booking.departure_location }}</td> + <td>{{ booking.destination_location }}</td> + <td class="dt-center"> + <div class="dropdown"> + <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false"> + Actions + </button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> + <a class="dropdown-item edit-btn" href="#">View</a> + <a class="dropdown-item delete-btn" href="#">Cancel Booking</a> + </div> + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <!-- Cancel booking modal --> + <div class="modal fade" id="confirm_booking_deletion" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="confirm_booking_deletion">Confirm Cancellation</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Type 'CONFIRM' to cancel your booking:</p> + <input type="text" id="confirmation_input" class="form-control"> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + <button type="button" class="btn btn-primary" id="confirm_deletion_button">Cancel Booking</button> + </div> + </div> + </div> + </div> + <!-- Filter modal --> + <div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="filterModalLabel"><i class="fa-solid fa-filter"></i> Filter Options</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <form id="filter-form"> + <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> + </div> + <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> + </div> + <div class="mb-3"> + <label for="depart_date" class="form-label">Depart Date:</label> + <input type="date" class="form-control" id="depart_date" name="depart_date"> + </div> + <div class="mb-3"> + <label for="booking_date" class="form-label">Booking Date:</label> + <input type="date" class="form-control" id="booking_date" name="booking_date"> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + <button type="button" class="btn btn-primary" id="apply-filters">Apply Filters</button> + </div> + </div> + </div> + </div> </div> -{% endblock %} \ No newline at end of file + +<style> + .table-container { + width: 100%; + overflow-x: auto; + overflow-y: hidden; + } + + @media (max-width: 800px) { + .table-container { + padding: 0 10px; + } + } + + .dataTables_wrapper { + width: 100%; + } + + table.dataTable.no-footer { + margin-bottom: 30px; + } +</style> +<script> + $(document).ready(function() { + $('.select2-multiple').select2({ + placeholder: "Select locations", + width: '100%' + }); + + $('.select2-dropdown').select2({ + placeholder: "Select a date", + width: '100%', + minimumResultsForSearch: Infinity + }); + + // Populate location options, TO UPDATE WITH LIVE LOCATIONS + const locations = JSON.parse('{{ locations|tojson|safe }}'); + locations.forEach(location => { + $('#depart_location').append(new Option(location, location)); + $('#destination_location').append(new Option(location, location)); + }); + + // Load table + const table = $('#manage_bookings').DataTable({ + pageLength: 10, + lengthChange: false, + searching: false, + ordering: false, + ajax: { + url: "{{ url_for('bookings.get_user_bookings') }}", + dataSrc: '', + data: function(d) { + d.depart_location = $('#depart_location').val() ? $('#depart_location').val().join(',') : ''; + d.destination_location = $('#destination_location').val() ? $('#destination_location').val().join(',') : ''; + d.depart_date = $('#depart_date').val(); + d.booking_date = $('#booking_date').val(); + } + }, + columns: [ + { data: 'id', visible: false }, // Hidden id column + { data: 'booking_date' }, + { data: 'depart_date' }, + { data: 'depart_location' }, + { data: 'destination_location' }, + { + data: null, + className: "dt-center", + defaultContent: ` + <div class="dropdown"> + <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false"> + Actions + </button> + <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> + <a class="dropdown-item edit-btn" href="#">View</a> + <a class="dropdown-item delete-btn" href="#">Cancel Booking</a> + </div> + </div>` + } + ], + language: { + emptyTable: "No bookings could be found." + }, + createdRow: function(row, data, dataIndex) { + $(row).find('.edit-btn').attr('data-id', data.id); + $(row).find('.delete-btn').attr('data-id', data.id); + } + }); + + $('#apply-filters').on('click', function() { + table.ajax.reload(); + $('#filterModal').modal('hide'); + }); + + $('#manage_bookings tbody').on('click', '.edit-btn', function() { + const id = $(this).data('id'); + window.location.href = `manage_bookings/view/${id}`; + }); + + let delete_booking_id; + $('#manage_bookings tbody').on('click', '.delete-btn', function() { + delete_booking_id = $(this).data('id'); + $('#confirm_booking_deletion').modal('show'); + }); + + $('#confirm_deletion_button').on('click', function() { + const confirmationInput = $('#confirmation_input').val(); + if (confirmationInput === 'CONFIRM') { + $.ajax({ + url: `{{ url_for('profile.cancel_booking') }}`, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ booking_id: delete_booking_id }), + success: function(response) { + $('#confirm_booking_deletion').modal('hide'); + table.ajax.reload(); + }, + error: function(xhr, status, error) { + alert('An error occurred while cancelling the booking.'); + } + }); + } else { + alert("Please type 'CONFIRM' to proceed."); + } + }); + }); +</script> +{% endblock %} diff --git a/app/templates/profile/manage_profile.html b/app/templates/profile/manage_profile.html index 87968615832181b56e9af5ffde63a287d246f8c9..054dea26da679cf166afec2e68ed8ed5f44b4eb4 100644 --- a/app/templates/profile/manage_profile.html +++ b/app/templates/profile/manage_profile.html @@ -1,26 +1,24 @@ {% extends 'base.html' %} {% block content %} -<div class="container"> - <div class="mt-5"> - <h1 class="display-4">User Profile</h1> - <div> - <button class="btn btn-info mb-3" id="editButton" onclick="enableEditing()"> - <i class="fas fa-wrench"></i> Edit - </button> - <form id="updateForm" action="{{ url_for('profile.manage_profile') }}" method="post" onsubmit="return showModal()"> - <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> - <div class="form-group"> - <label for="username">Username</label> - <input type="text" class="form-control" id="username" name="username" value="{{ user.username }}" disabled required> - </div> - <div class="form-group"> - <label for="email">Email</label> - <input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" disabled required> - </div> - <button type="submit" class="btn btn-primary" disabled>Update Info</button> - </form> - <a href="{{ url_for('profile.password_reset_process') }}" class="btn btn-warning"><i class="fas fa-key"></i> Reset Password</a> +<div class="container mt-5"> + <h1 class="display-4 mb-4">User Profile</h1> + <form id="updateForm" action="{{ url_for('profile.manage_profile') }}" method="post" onsubmit="return showModal()"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <div class="form-group"> + <label for="username">Username</label> + <input type="text" class="form-control" id="username" name="username" value="{{ user.username }}" disabled required> </div> + <div class="form-group"> + <label for="email">Email</label> + <input type="email" class="form-control" id="email" name="email" value="{{ user.email }}" disabled required> + </div> + <button type="submit" class="btn btn-primary d-none mt-3" id="updateButton">Update Info</button> + </form> + <div class="d-flex justify-content-start mt-3"> + <button class="btn btn-dark mr-3" id="editButton" style="margin-right:10px" onclick="enableEditing()"> + <i class="fas fa-wrench"></i> Edit + </button> + <a href="{{ url_for('profile.password_reset_process') }}" class="btn btn-warning ml-2"><i class="fas fa-key"></i> Reset Password</a> </div> </div> @@ -30,16 +28,14 @@ <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="confirmModalLabel">Confirm Update</h5> - <button type="button" class="close" data-dismiss="modal" aria-label="Close"> - <span aria-hidden="true">×</span> - </button> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <p>Type "CONFIRM" to proceed with the update.</p> <input type="text" class="form-control" id="confirmInput" required> </div> <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="hideModal()">Cancel</button> <button type="button" class="btn btn-primary" onclick="submitForm()">Confirm</button> </div> </div> @@ -50,7 +46,7 @@ function enableEditing() { document.getElementById('username').disabled = false; document.getElementById('email').disabled = false; - document.querySelector('button[type="submit"]').disabled = false; + document.getElementById('updateButton').classList.remove('d-none'); } function showModal() { @@ -58,6 +54,10 @@ return false; } + function hideModal() { + $('#confirmModal').modal('hide'); + } + function submitForm() { var confirmInput = document.getElementById('confirmInput').value; if (confirmInput === 'CONFIRM') { diff --git a/app/templates/profile/view_booking.html b/app/templates/profile/view_booking.html new file mode 100644 index 0000000000000000000000000000000000000000..8425a68b5c95881e64925fe0df6c2282e11a2f10 --- /dev/null +++ b/app/templates/profile/view_booking.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block content %} +<div class="column is-4 is-offset-4"> + <div id="login-box" class="form_box_30" style="margin-top: 30px;"> + <div class="profile_form_background"> + <h2 class="form_header">Login</h2> + <form method="POST" action="{{ url_for('profile.login_post') }}"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <div class="mb-3"> + <label class="form-label" autofocus="">Username/Email</label> + <input type="text" class="form-control" name="username"> + </div> + <div class="mb-3"> + <label for="exampleInputPassword1" class="form-label">Password</label> + <input type="password" class="form-control" name="password"> + </div> + <div class="mb-3"> + <a class="clear-hyperlink" href="{{ url_for('profile.password_reset') }}">Forgot Password?</a> + </div> + <div class="mb-3 form-check"> + <input type="checkbox" class="form-check-input" id="remember" name="remember"> + <label class="form-check-label" for="remember">Remember me</label> + </div> + <button type="submit" class="btn btn-primary">Log In</button> + </form> + </div> + </div> +</div> +{% endblock %}