From aa928c8dc3792d9eb59d99eb0d76c654af63f914 Mon Sep 17 00:00:00 2001 From: Ethan Clay <Ethan2.Clay@live.uwe.ac.uk> Date: Fri, 21 Feb 2025 10:26:42 +0000 Subject: [PATCH] Add reporting tab --- app/admin/routes.py | 42 +++++++- app/models/bookings.py | 6 +- app/models/user.py | 31 ++++++ app/templates/admin/index.html | 1 + app/templates/admin/reports.html | 167 +++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 app/templates/admin/reports.html diff --git a/app/admin/routes.py b/app/admin/routes.py index 9ca30fe..7f3f80a 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,10 +1,10 @@ from flask import render_template, redirect, url_for, request, jsonify, flash -from app import db -from app import admin_permission, permission_required, super_admin_permission -from app.models import Listings, ListingImages, User -from app.admin import bp -from app.main.utils import generate_time_options +from datetime import datetime, timedelta from sqlalchemy.sql import text +from app import admin_permission, permission_required, super_admin_permission, db +from app.models import Listings, ListingImages, User, Bookings +from app.main.utils import generate_time_options +from app.admin import bp @bp.route('/home') @@ -52,12 +52,44 @@ def edit_user(id): 'admin/edit_user.html', user=user ) + + +@bp.route('/reports') +def reports(): + days = int(request.args.get('days', 30)) + start_date = datetime.now() - timedelta(days=days) + + total_revenue = db.session.query(db.func.sum(Bookings.amount_paid)).filter(Bookings.booking_date >= start_date).scalar() + cancelled_bookings = db.session.query(db.func.count(Bookings.id)).filter(Bookings.cancelled == True, Bookings.booking_date >= start_date).scalar() + + destinations = db.session.query(Listings.destination_location, db.func.count(Bookings.id)).join(Bookings, Listings.id == Bookings.listing_id).filter(Bookings.booking_date >= start_date).group_by(Listings.destination_location).all() + + user_counts = list(User.get_role_counts()) + + unformatted_seats_booked_per_day = db.session.query(Bookings.booking_date, db.func.sum(Bookings.num_seats)).filter(Bookings.booking_date >= start_date).group_by(Bookings.booking_date).all() + seats_booked_per_day = [(date.strftime('%d/%m/%Y'), count) for date, count in unformatted_seats_booked_per_day] + + unformatted_depart_dates = db.session.query(Bookings.depart_date, db.func.count(Bookings.id)).filter(Bookings.depart_date >= start_date).group_by(Bookings.depart_date).all() + depart_dates = [(date.strftime('%d/%m/%Y'), count) for date, count in unformatted_depart_dates] + + return render_template( + 'admin/reports.html', + total_revenue=total_revenue, + cancelled_bookings=cancelled_bookings, + destinations=destinations, + user_counts=user_counts, + seats_booked_per_day=seats_booked_per_day, + depart_dates=depart_dates, + days=days + ) + @bp.route('/manage_users') @permission_required(super_admin_permission) def manage_users(): return render_template('admin/manage_users.html') + @bp.route('/manage_user_bookings') @permission_required(admin_permission) def manage_user_bookings(): diff --git a/app/models/bookings.py b/app/models/bookings.py index f9834f2..d7ba805 100644 --- a/app/models/bookings.py +++ b/app/models/bookings.py @@ -68,4 +68,8 @@ class Bookings(UserMixin, db.Model): booking.cancelled_date = datetime.utcnow().date() db.session.commit() return True - return False \ No newline at end of file + return False + + @classmethod + def get_all_bookings(cls): + return cls.query.all() \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index fd4bc18..6d819bd 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -85,3 +85,34 @@ 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 + + + @classmethod + def get_role_counts(cls, roles=()): + from app.models import Role + result = [] + if roles: + for role_name in roles: + role = Role.query.filter_by(name=role_name).first() + if role: + count = cls.query.filter_by(role_id=role.id).count() + result.append((role_name, count)) + else: + all_roles = Role.query.all() + for role in all_roles: + count = cls.query.filter_by(role_id=role.id).count() + result.append((role.name, count)) + return tuple(result) \ No newline at end of file diff --git a/app/templates/admin/index.html b/app/templates/admin/index.html index f08f398..8ac1c6f 100644 --- a/app/templates/admin/index.html +++ b/app/templates/admin/index.html @@ -7,6 +7,7 @@ <h2>Admin Panel</h2> <ul class="center"> + <li><a class="button_1" href="{{ url_for('admin.reports') }}">Reports</a></li> <li><a class="button_1" href="{{ url_for('admin.manage_bookings') }}">Manage Bookings</a></li> <li><a class="button_1" href="{{ url_for('admin.manage_user_bookings') }}">Manage User Bookings</a></li> {% if g.is_super_admin %} diff --git a/app/templates/admin/reports.html b/app/templates/admin/reports.html new file mode 100644 index 0000000..2b702bf --- /dev/null +++ b/app/templates/admin/reports.html @@ -0,0 +1,167 @@ +{% extends 'base.html' %} +{% block content %} +<!DOCTYPE html> +<html> +<head> + <title>Reporting</title> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> + <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> +</head> +<body> + <div class="container"> + <h1 class="text-center my-4">Reporting</h1> + + <form method="get" action="{{ url_for('admin.reports') }}" class="form-inline mb-4"> + <div class="form-group mx-sm-3 mb-2"> + <label for="days" class="sr-only">Show results for the last</label> + <input type="number" class="form-control" id="days" name="days" value="{{ days }}" min="1" placeholder="Days"> + </div> + <button type="submit" class="btn btn-primary mb-2">Update</button> + </form> + + <div class="row mb-4"> + <div class="col-md-6"> + <div class="card text-white bg-info mb-3"> + <div class="card-body"> + <h5 class="card-title">Total Revenue</h5> + <p class="card-text">£ {{ total_revenue }}</p> + </div> + </div> + </div> + <div class="col-md-6"> + <div class="card text-white bg-danger mb-3"> + <div class="card-body"> + <h5 class="card-title">Cancelled Bookings</h5> + <p class="card-text">{{ cancelled_bookings }}</p> + </div> + </div> + </div> + </div> + + <div class="row"> + <div class="col-md-6 mb-4"> + <h3>Destination Locations</h3> + <canvas id="destinationChart" height="200"></canvas> + </div> + <div class="col-md-6 mb-4"> + <h3>User Count</h3> + <canvas id="roleCountChart" height="200"></canvas> + </div> + </div> + <div class="row"> + <div class="col-md-6 mb-4"> + <h3>Number of Seats Booked Per Day</h3> + <canvas id="seatsBookedChart" height="200"></canvas> + </div> + <div class="col-md-6 mb-4"> + <h3>Depart Dates</h3> + <canvas id="departDateChart" height="200"></canvas> + </div> + </div> + </div> + <script> + // Destination Locations Pie Chart + var destinationData = { + labels: {{ destinations|map(attribute=0)|list|tojson }}, + datasets: [{ + label: 'Number of Bookings', + data: {{ destinations|map(attribute=1)|list|tojson }}, + backgroundColor: [ + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ], + borderColor: [ + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ], + borderWidth: 1 + }] + }; + + var ctx = document.getElementById('destinationChart').getContext('2d'); + var destinationChart = new Chart(ctx, { + type: 'pie', + data: destinationData + }); + + // User Role Count Bar Chart + var roleCountData = { + labels: {{ user_counts|map(attribute=0)|list|tojson }}, + datasets: [{ + label: 'User Count', + data: {{ user_counts|map(attribute=1)|list|tojson }}, + backgroundColor: 'rgba(153, 102, 255, 0.2)', + borderColor: 'rgba(153, 102, 255, 1)', + borderWidth: 1 + }] + }; + + var ctx = document.getElementById('roleCountChart').getContext('2d'); + var roleCountChart = new Chart(ctx, { + type: 'bar', + data: roleCountData, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + + // Number of Seats Booked Per Day Line Chart + var seatsBookedData = { + labels: {{ seats_booked_per_day|map(attribute=0)|list|tojson }}, + datasets: [{ + label: 'Number of Seats Booked', + data: {{ seats_booked_per_day|map(attribute=1)|list|tojson }}, + backgroundColor: 'rgba(54, 162, 235, 0.2)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 1 + }] + }; + + var ctx = document.getElementById('seatsBookedChart').getContext('2d'); + var seatsBookedChart = new Chart(ctx, { + type: 'bar', + data: seatsBookedData, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + + // Depart Dates Line Chart + var departDateData = { + labels: {{ depart_dates|map(attribute=0)|list|tojson }}, + datasets: [{ + label: 'Number of Departures', + data: {{ depart_dates|map(attribute=1)|list|tojson }}, + backgroundColor: 'rgba(255, 206, 86, 0.2)', + borderColor: 'rgba(255, 206, 86, 1)', + borderWidth: 1 + }] + }; + + var ctx = document.getElementById('departDateChart').getContext('2d'); + var departDateChart = new Chart(ctx, { + type: 'bar', + data: departDateData, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + </script> +</body> +</html> + +{% endblock %} -- GitLab