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