Equipment booking app

avatar
(Edited)

I love to demonstrate to my colleagues how a bit of coding can reduce tedious and repetitive work. We had a recent conversation about how booking of lab equipment could be improved.

In my eyes, codes solve (almost) everything!

So I got on to tinker this morning, and my AI pet made me a prototype of an equipment booking app that I can show my colleagues next week.

It's crazy how an equipment booking app that would take months to conceptualise and create now took me 10 minutes to create with generative AI. Here is the link (while it is there) https://app-keefellow.pythonanywhere.com/login
Screenshot 2025-04-06 at 9.26.19 AM.png

I am sharing the codes here, written as a Python Flask app.

The main steps are as follows:

  • Import necessary Flask modules and other libraries (CSV, datetime, etc.)
  • Create a Flask application with a secret key for session management
  • Define user credentials with different roles (user and manager)
  • Define paths to CSV files for storing equipment and booking data

User Authentication System

  • Create login/logout functionality
  • Implement decorators to protect routes based on authentication status
  • Create separate decorator for manager-only access
  • Store user session data (username and role)

Data Management

  • Implement functions to load and save equipment data to CSV
  • Implement functions to load and save booking data to CSV
  • Auto-create sample data files if they don't exist on first run
  • Handle proper type conversion for numeric fields

Route Structure

  • Home route for redirection to appropriate page based on login status
  • Login/logout routes for authentication
  • Equipment listing route with search functionality
  • Routes for booking, viewing, and managing equipment
  • Special routes for managers (all bookings, approve/reject, etc.)

Equipment Management

  • Display equipment with availability information
  • Add search functionality with filtering
  • Allow managers to add new equipment
  • Allow managers to edit existing equipment with validation
  • Prevent reducing quantities below currently booked amounts

Booking System

  • Implement booking functionality with validations
  • Track equipment availability based on bookings
  • Allow users to view and manage their own bookings
  • Allow users to cancel pending bookings
  • Allow users to return approved equipment
  • Allow managers to approve/reject pending bookings

User Interface

  • Create HTML templates with CSS styling inline
  • Implement different views based on user roles
  • Create consistent navigation between different sections
  • Implement status indicators for bookings
  • Display appropriate action buttons based on booking status

Error Handling

  • Implement form validation for all inputs
  • Display flash messages for successful operations and errors
  • Handle edge cases like insufficient quantity, invalid dates, etc.
from flask import Flask, render_template_string, request, redirect, url_for, session, flash
import csv
import os
import secrets
from functools import wraps
from datetime import datetime

app = Flask(__name__)
app.secret_key = secrets.token_hex(16)  # Set a secret key for session management

# User credentials (in a real app, you would use a database and proper password hashing)
USERS = {
    'user': {'password': 'userpass', 'role': 'user'},
    'manager': {'password': 'managerpass', 'role': 'manager'}
}

# Path to CSV files
EQUIPMENT_FILE = 'equipment.csv'
BOOKING_FILE = 'bookings.csv'

# Login required decorator
def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session:
            flash('Please log in to access this page')
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

# Manager role required decorator
def manager_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'role' not in session or session['role'] != 'manager':
            flash('You need manager permissions to access this page')
            return redirect(url_for('index'))
        return f(*args, **kwargs)
    return decorated_function

def load_equipment():
    """Load equipment data from CSV file"""
    equipment_list = []

    # Create a sample equipment file if it doesn't exist
    if not os.path.exists(EQUIPMENT_FILE):
        with open(EQUIPMENT_FILE, 'w', newline='') as file:
            writer = csv.writer(file)
            writer.writerow(['id', 'name', 'quantity', 'available'])
            for i in range(1, 11):
                writer.writerow([i, f'Equipment {i}', 5, 5])

    # Read the equipment data
    with open(EQUIPMENT_FILE, 'r') as file:
        reader = csv.DictReader(file)
        for row in reader:
            equipment_list.append({
                'id': row['id'],
                'name': row['name'],
                'quantity': int(row['quantity']),
                'available': int(row['available']) if 'available' in row else int(row['quantity'])
            })

    return equipment_list

def save_equipment(equipment_list):
    """Save equipment data to CSV file"""
    with open(EQUIPMENT_FILE, 'w', newline='') as file:
        writer = csv.DictWriter(file, fieldnames=['id', 'name', 'quantity', 'available'])
        writer.writeheader()
        for item in equipment_list:
            writer.writerow(item)

def load_bookings():
    """Load bookings data from CSV file"""
    bookings = []

    # Create bookings file if it doesn't exist
    if not os.path.exists(BOOKING_FILE):
        with open(BOOKING_FILE, 'w', newline='') as file:
            writer = csv.writer(file)
            writer.writerow(['id', 'equipment_id', 'equipment_name', 'user', 'quantity',
                             'start_date', 'end_date', 'status'])

    # Read the bookings data
    with open(BOOKING_FILE, 'r') as file:
        reader = csv.DictReader(file)
        for row in reader:
            bookings.append({
                'id': row['id'],
                'equipment_id': row['equipment_id'],
                'equipment_name': row['equipment_name'],
                'user': row['user'],
                'quantity': int(row['quantity']),
                'start_date': row['start_date'],
                'end_date': row['end_date'],
                'status': row['status']
            })

    return bookings

def save_bookings(bookings):
    """Save bookings data to CSV file"""
    with open(BOOKING_FILE, 'w', newline='') as file:
        writer = csv.DictWriter(file, fieldnames=[
            'id', 'equipment_id', 'equipment_name', 'user', 'quantity',
            'start_date', 'end_date', 'status'
        ])
        writer.writeheader()
        for booking in bookings:
            writer.writerow(booking)

@app.route('/')
def home():
    """Redirect to login if not logged in, otherwise to equipment list"""
    if 'username' in session:
        return redirect(url_for('index'))
    return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    """Handle user login"""
    error = None

    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        if username in USERS and USERS[username]['password'] == password:
            session['username'] = username
            session['role'] = USERS[username]['role']
            flash(f'Welcome, {username}!')
            return redirect(url_for('index'))
        else:
            error = 'Invalid username or password. Please try again.'

    # Login page template
    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Login - Equipment System</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 500px;
                margin: 50px auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
                border-radius: 5px;
            }
            h1 {
                text-align: center;
                margin-bottom: 20px;
            }
            .form-group {
                margin-bottom: 15px;
            }
            label {
                display: block;
                margin-bottom: 5px;
                font-weight: bold;
            }
            input[type="text"], input[type="password"] {
                width: 100%;
                padding: 8px;
                border: 1px solid #ddd;
                border-radius: 4px;
                box-sizing: border-box;
            }
            button {
                display: block;
                width: 100%;
                padding: 10px;
                background: #4CAF50;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 16px;
            }
            button:hover {
                background: #45a049;
            }
            .error {
                color: red;
                margin-bottom: 15px;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
            .demo-info {
                margin-top: 20px;
                padding: 10px;
                background-color: #f8f9fa;
                border-radius: 4px;
                border-left: 4px solid #6c757d;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>Equipment System Login</h1>

            {% if error %}
            <div class="error">{{ error }}</div>
            {% endif %}

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            <form method="post">
                <div class="form-group">
                    <label for="username">Username:</label>
                    <input type="text" id="username" name="username" required>
                </div>
                <div class="form-group">
                    <label for="password">Password:</label>
                    <input type="password" id="password" name="password" required>
                </div>
                <button type="submit">Log In</button>
            </form>

            <div class="demo-info">
                <strong>For demonstration purposes:</strong><br>
                <p>User login: username = "user", password = "userpass"</p>
                <p>Manager login: username = "manager", password = "managerpass"</p>
            </div>
        </div>
    </body>
    </html>
    """

    return render_template_string(template, error=error)

@app.route('/logout')
def logout():
    """Handle user logout"""
    session.clear()
    flash('You have been logged out successfully')
    return redirect(url_for('login'))

@app.route('/equipment', methods=['GET'])
@login_required
def index():
    """Home page - show equipment list with search functionality"""
    equipment_list = load_equipment()
    search_query = request.args.get('search', '').lower()

    # Filter equipment based on search query if provided
    if search_query:
        filtered_equipment = [
            item for item in equipment_list
            if search_query in item['name'].lower() or search_query in str(item['id'])
        ]
    else:
        filtered_equipment = equipment_list

    # HTML template as a string
    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Equipment Availability</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 900px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 6px 12px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-logout {
                background: #dc3545;
            }
            .btn-info {
                background: #17a2b8;
            }
            .btn-primary {
                background: #007bff;
            }
            .nav {
                display: flex;
                gap: 10px;
                margin-bottom: 20px;
            }
            table {
                width: 100%;
                border-collapse: collapse;
            }
            table th, table td {
                padding: 10px;
                border: 1px solid #ddd;
                text-align: left;
            }
            table th {
                background-color: #f4f4f4;
            }
            .search-box {
                margin-bottom: 20px;
            }
            .search-input {
                padding: 8px;
                width: 300px;
                border: 1px solid #ddd;
                border-radius: 4px;
            }
            .clear-link {
                margin-left: 10px;
                text-decoration: none;
                color: #666;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>Equipment Availability</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong> ({{ session.role }})
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            <div class="nav">
                <a href="{{ url_for('index') }}" class="btn btn-primary">Equipment</a>
                <a href="{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings</a>
                {% if session.role == 'manager' %}
                <a href="{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings</a>
                <a href="{{ url_for('add_equipment') }}" class="btn">Add Equipment</a>
                {% endif %}
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            <div class="search-box">
                <form method="GET" action="{{ url_for('index') }}">
                    <input type="text" name="search" placeholder="Search by name or ID"
                           value="{{ search_query }}" class="search-input">
                    <button type="submit" class="btn">Search</button>
                    {% if search_query %}
                    <a href="{{ url_for('index') }}" class="clear-link">Clear</a>
                    {% endif %}
                </form>
            </div>

            {% if equipment|length == 0 %}
            <p>No equipment found matching your search.</p>
            {% else %}
            <table>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Total Quantity</th>
                    <th>Available</th>
                    <th>Actions</th>
                </tr>
                {% for item in equipment %}
                <tr>
                    <td>{{ item.id }}</td>
                    <td>{{ item.name }}</td>
                    <td>{{ item.quantity }}</td>
                    <td>{{ item.available }}</td>
                    <td>
                        {% if item.available > 0 %}
                        <a href="{{ url_for('book_equipment', equipment_id=item.id) }}" class="btn">Book</a>
                        {% else %}
                        <span>Not Available</span>
                        {% endif %}

                        {% if session.role == 'manager' %}
                        <a href="{{ url_for('edit_equipment', equipment_id=item.id) }}" class="btn btn-info">Edit</a>
                        {% endif %}
                    </td>
                </tr>
                {% endfor %}
            </table>
            {% endif %}
        </div>
    </body>
    </html>
    """

    return render_template_string(template, equipment=filtered_equipment, search_query=search_query)

@app.route('/book/<equipment_id>', methods=['GET', 'POST'])
@login_required
def book_equipment(equipment_id):
    """Book equipment form and processing"""
    equipment_list = load_equipment()
    equipment = next((item for item in equipment_list if item['id'] == equipment_id), None)

    if not equipment:
        flash('Equipment not found')
        return redirect(url_for('index'))

    if request.method == 'POST':
        try:
            quantity = int(request.form.get('quantity', 1))
            start_date = request.form.get('start_date')
            end_date = request.form.get('end_date')

            if not start_date or not end_date:
                flash('Please provide both start and end dates')
                return redirect(url_for('book_equipment', equipment_id=equipment_id))

            if quantity <= 0:
                flash('Quantity must be at least 1')
                return redirect(url_for('book_equipment', equipment_id=equipment_id))

            if quantity > equipment['available']:
                flash('Not enough equipment available')
                return redirect(url_for('book_equipment', equipment_id=equipment_id))

            # All validation passed, create booking
            bookings = load_bookings()

            # Generate new booking ID
            new_id = 1
            if bookings:
                max_id = max(int(booking['id']) for booking in bookings)
                new_id = max_id + 1

            # Create new booking
            new_booking = {
                'id': str(new_id),
                'equipment_id': equipment_id,
                'equipment_name': equipment['name'],
                'user': session['username'],
                'quantity': quantity,
                'start_date': start_date,
                'end_date': end_date,
                'status': 'approved' if session['role'] == 'manager' else 'pending'
            }

            bookings.append(new_booking)
            save_bookings(bookings)

            # Update equipment availability
            equipment['available'] -= quantity
            save_equipment(equipment_list)

            flash(f'Successfully booked {quantity} {equipment["name"]}(s)')
            return redirect(url_for('my_bookings'))

        except ValueError:
            flash('Please enter valid quantity')
            return redirect(url_for('book_equipment', equipment_id=equipment_id))

    # GET request - show booking form
    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Book Equipment</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 700px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 8px 16px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-secondary {
                background: #6c757d;
            }
            .btn-logout {
                background: #dc3545;
                padding: 6px 12px;
                margin-left: 10px;
            }
            .form-group {
                margin-bottom: 15px;
            }
            label {
                display: block;
                margin-bottom: 5px;
                font-weight: bold;
            }
            input {
                width: 100%;
                padding: 8px;
                border: 1px solid #ddd;
                border-radius: 4px;
                box-sizing: border-box;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
            .equipment-details {
                background-color: #f8f9fa;
                padding: 15px;
                border-radius: 4px;
                margin-bottom: 20px;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>Book Equipment</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong>
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            <div class="equipment-details">
                <h3>{{ equipment.name }}</h3>
                <p><strong>Available Quantity:</strong> {{ equipment.available }} of {{ equipment.quantity }}</p>
            </div>

            <form method="post">
                <div class="form-group">
                    <label for="quantity">Quantity to Book:</label>
                    <input type="number" id="quantity" name="quantity" min="1" max="{{ equipment.available }}" value="1" required>
                </div>

                <div class="form-group">
                    <label for="start_date">Start Date:</label>
                    <input type="date" id="start_date" name="start_date" required>
                </div>

                <div class="form-group">
                    <label for="end_date">End Date:</label>
                    <input type="date" id="end_date" name="end_date" required>
                </div>

                <div style="display: flex; gap: 10px;">
                    <button type="submit" class="btn">Book Now</button>
                    <a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
                </div>
            </form>
        </div>
    </body>
    </html>
    """

    return render_template_string(template, equipment=equipment)

@app.route('/my-bookings')
@login_required
def my_bookings():
    """Show user's bookings"""
    bookings = load_bookings()
    user_bookings = [b for b in bookings if b['user'] == session['username']]

    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>My Bookings</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 900px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 6px 12px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-logout {
                background: #dc3545;
            }
            .btn-info {
                background: #17a2b8;
            }
            .btn-primary {
                background: #007bff;
            }
            .nav {
                display: flex;
                gap: 10px;
                margin-bottom: 20px;
            }
            table {
                width: 100%;
                border-collapse: collapse;
            }
            table th, table td {
                padding: 10px;
                border: 1px solid #ddd;
                text-align: left;
            }
            table th {
                background-color: #f4f4f4;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
            .status {
                padding: 4px 8px;
                border-radius: 4px;
                font-size: 0.8em;
                font-weight: bold;
            }
            .status-approved {
                background-color: #d4edda;
                color: #155724;
            }
            .status-pending {
                background-color: #fff3cd;
                color: #856404;
            }
            .status-rejected {
                background-color: #f8d7da;
                color: #721c24;
            }
            .status-returned {
                background-color: #d1ecf1;
                color: #0c5460;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>My Bookings</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong> ({{ session.role }})
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            <div class="nav">
                <a href="{{ url_for('index') }}" class="btn btn-primary">Equipment</a>
                <a href="{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings</a>
                {% if session.role == 'manager' %}
                <a href="{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings</a>
                <a href="{{ url_for('add_equipment') }}" class="btn">Add Equipment</a>
                {% endif %}
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            {% if bookings|length == 0 %}
            <p>You don't have any bookings yet.</p>
            <p><a href="{{ url_for('index') }}" class="btn">Book Equipment</a></p>
            {% else %}
            <table>
                <tr>
                    <th>ID</th>
                    <th>Equipment</th>
                    <th>Quantity</th>
                    <th>Start Date</th>
                    <th>End Date</th>
                    <th>Status</th>
                    <th>Actions</th>
                </tr>
                {% for booking in bookings %}
                <tr>
                    <td>{{ booking.id }}</td>
                    <td>{{ booking.equipment_name }}</td>
                    <td>{{ booking.quantity }}</td>
                    <td>{{ booking.start_date }}</td>
                    <td>{{ booking.end_date }}</td>
                    <td>
                        <span class="status status-{{ booking.status }}">
                            {{ booking.status|upper }}
                        </span>
                    </td>
                    <td>
                        {% if booking.status == "approved" %}
                        <a href="{{ url_for('return_equipment', booking_id=booking.id) }}" class="btn">Return</a>
                        {% elif booking.status == "pending" %}
                        <a href="{{ url_for('cancel_booking', booking_id=booking.id) }}" class="btn">Cancel</a>
                        {% endif %}
                    </td>
                </tr>
                {% endfor %}
            </table>
            {% endif %}
        </div>
    </body>
    </html>
    """

    return render_template_string(template, bookings=user_bookings)

@app.route('/all-bookings')
@login_required
@manager_required
def all_bookings():
    """Show all bookings (manager only)"""
    bookings = load_bookings()

    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>All Bookings</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 1000px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 6px 12px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-logout {
                background: #dc3545;
            }
            .btn-info {
                background: #17a2b8;
            }
            .btn-primary {
                background: #007bff;
            }
            .btn-danger {
                background: #dc3545;
            }
            .nav {
                display: flex;
                gap: 10px;
                margin-bottom: 20px;
            }
            table {
                width: 100%;
                border-collapse: collapse;
            }
            table th, table td {
                padding: 10px;
                border: 1px solid #ddd;
                text-align: left;
            }
            table th {
                background-color: #f4f4f4;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
            .status {
                padding: 4px 8px;
                border-radius: 4px;
                font-size: 0.8em;
                font-weight: bold;
            }
            .status-approved {
                background-color: #d4edda;
                color: #155724;
            }
            .status-pending {
                background-color: #fff3cd;
                color: #856404;
            }
            .status-rejected {
                background-color: #f8d7da;
                color: #721c24;
            }
            .status-returned {
                background-color: #d1ecf1;
                color: #0c5460;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>All Bookings</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong> (Manager)
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            <div class="nav">
                <a href="{{ url_for('index') }}" class="btn btn-primary">Equipment</a>
                <a href="{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings</a>
                <a href="{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings</a>
                <a href="{{ url_for('add_equipment') }}" class="btn">Add Equipment</a>
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            {% if bookings|length == 0 %}
            <p>There are no bookings in the system.</p>
            {% else %}
            <table>
                <tr>
                    <th>ID</th>
                    <th>User</th>
                    <th>Equipment</th>
                    <th>Quantity</th>
                    <th>Start Date</th>
                    <th>End Date</th>
                    <th>Status</th>
                    <th>Actions</th>
                </tr>
                {% for booking in bookings %}
                <tr>
                    <td>{{ booking.id }}</td>
                    <td>{{ booking.user }}</td>
                    <td>{{ booking.equipment_name }}</td>
                    <td>{{ booking.quantity }}</td>
                    <td>{{ booking.start_date }}</td>
                    <td>{{ booking.end_date }}</td>
                    <td>
                        <span class="status status-{{ booking.status }}">
                            {{ booking.status|upper }}
                        </span>
                    </td>
                    <td>
                        {% if booking.status == "pending" %}
                        <a href="{{ url_for('approve_booking', booking_id=booking.id) }}" class="btn">Approve</a>
                        <a href="{{ url_for('reject_booking', booking_id=booking.id) }}" class="btn btn-danger">Reject</a>
                        {% endif %}
                    </td>
                </tr>
                {% endfor %}
            </table>
            {% endif %}
        </div>
    </body>
    </html>
    """

    return render_template_string(template, bookings=bookings)

@app.route('/approve-booking/<booking_id>')
@login_required
@manager_required
def approve_booking(booking_id):
    """Approve a pending booking (manager only)"""
    bookings = load_bookings()
    booking = next((b for b in bookings if b['id'] == booking_id and b['status'] == 'pending'), None)

    if booking:
        booking['status'] = 'approved'
        save_bookings(bookings)
        flash('Booking approved successfully')
    else:
        flash('Booking not found or not in pending status')

    return redirect(url_for('all_bookings'))

@app.route('/reject-booking/<booking_id>')
@login_required
@manager_required
def reject_booking(booking_id):
    """Reject a pending booking (manager only)"""
    bookings = load_bookings()
    booking = next((b for b in bookings if b['id'] == booking_id and b['status'] == 'pending'), None)

    if booking:
        # Return equipment to available pool
        equipment_list = load_equipment()
        equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)

        if equipment:
            equipment['available'] += booking['quantity']
            save_equipment(equipment_list)

        booking['status'] = 'rejected'
        save_bookings(bookings)
        flash('Booking rejected successfully')
    else:
        flash('Booking not found or not in pending status')

    return redirect(url_for('all_bookings'))

@app.route('/cancel-booking/<booking_id>')
@login_required
def cancel_booking(booking_id):
    """Cancel a user's pending booking"""
    bookings = load_bookings()
    booking = next((b for b in bookings if b['id'] == booking_id and
                    b['user'] == session['username'] and
                    b['status'] == 'pending'), None)

    if booking:
        # Return equipment to available pool
        equipment_list = load_equipment()
        equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)

        if equipment:
            equipment['available'] += booking['quantity']
            save_equipment(equipment_list)

        # Remove the booking
        bookings.remove(booking)
        save_bookings(bookings)
        flash('Booking cancelled successfully')
    else:
        flash('Booking not found or cannot be cancelled')

    return redirect(url_for('my_bookings'))

@app.route('/return-equipment/<booking_id>')
@login_required
def return_equipment(booking_id):
    """Return equipment for an approved booking"""
    bookings = load_bookings()
    booking = next((b for b in bookings if b['id'] == booking_id and
                   b['user'] == session['username'] and
                   b['status'] == 'approved'), None)

    if booking:
        # Return equipment to available pool
        equipment_list = load_equipment()
        equipment = next((e for e in equipment_list if e['id'] == booking['equipment_id']), None)

        if equipment:
            equipment['available'] += booking['quantity']
            save_equipment(equipment_list)

        booking['status'] = 'returned'
        save_bookings(bookings)
        flash('Equipment returned successfully')
    else:
        flash('Booking not found or not in approved status')

    return redirect(url_for('my_bookings'))

@app.route('/add-equipment', methods=['GET', 'POST'])
@login_required
@manager_required
def add_equipment():
    """Add new equipment (manager only)"""
    if request.method == 'POST':
        name = request.form.get('name')
        try:
            quantity = int(request.form.get('quantity', 1))

            if not name or not name.strip():
                flash('Please provide a valid equipment name')
                return redirect(url_for('add_equipment'))

            if quantity <= 0:
                flash('Quantity must be at least 1')
                return redirect(url_for('add_equipment'))

            # All validation passed, add equipment
            equipment_list = load_equipment()

            # Generate new ID
            new_id = 1
            if equipment_list:
                max_id = max(int(item['id']) for item in equipment_list)
                new_id = max_id + 1

            # Create new equipment
            new_equipment = {
                'id': str(new_id),
                'name': name,
                'quantity': quantity,
                'available': quantity
            }

            equipment_list.append(new_equipment)
            save_equipment(equipment_list)

            flash(f'Successfully added {quantity} {name}(s)')
            return redirect(url_for('index'))

        except ValueError:
            flash('Please enter a valid quantity')
            return redirect(url_for('add_equipment'))

    # GET request - show add equipment form
    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Add Equipment</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 700px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 8px 16px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-secondary {
                background: #6c757d;
            }
            .btn-logout {
                background: #dc3545;
                padding: 6px 12px;
                margin-left: 10px;
            }
            .btn-info {
                background: #17a2b8;
            }
            .btn-primary {
                background: #007bff;
            }
            .nav {
                display: flex;
                gap: 10px;
                margin-bottom: 20px;
            }
            .form-group {
                margin-bottom: 15px;
            }
            label {
                display: block;
                margin-bottom: 5px;
                font-weight: bold;
            }
            input {
                width: 100%;
                padding: 8px;
                border: 1px solid #ddd;
                border-radius: 4px;
                box-sizing: border-box;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>Add New Equipment</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong> (Manager)
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            <div class="nav">
                <a href="{{ url_for('index') }}" class="btn btn-primary">Equipment</a>
                <a href="{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings</a>
                <a href="{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings</a>
                <a href="{{ url_for('add_equipment') }}" class="btn">Add Equipment</a>
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            <form method="post">
                <div class="form-group">
                    <label for="name">Equipment Name:</label>
                    <input type="text" id="name" name="name" required>
                </div>

                <div class="form-group">
                    <label for="quantity">Quantity:</label>
                    <input type="number" id="quantity" name="quantity" min="1" value="1" required>
                </div>

                <div style="display: flex; gap: 10px;">
                    <button type="submit" class="btn">Add Equipment</button>
                    <a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
                </div>
            </form>
        </div>
    </body>
    </html>
    """

    return render_template_string(template)

@app.route('/edit-equipment/<equipment_id>', methods=['GET', 'POST'])
@login_required
@manager_required
def edit_equipment(equipment_id):
    """Edit existing equipment (manager only)"""
    equipment_list = load_equipment()
    equipment = next((item for item in equipment_list if item['id'] == equipment_id), None)

    if not equipment:
        flash('Equipment not found')
        return redirect(url_for('index'))

    if request.method == 'POST':
        name = request.form.get('name')
        try:
            quantity = int(request.form.get('quantity', equipment['quantity']))

            if not name or not name.strip():
                flash('Please provide a valid equipment name')
                return redirect(url_for('edit_equipment', equipment_id=equipment_id))

            # Check if total quantity can be reduced (based on current bookings)
            booked_quantity = equipment['quantity'] - equipment['available']
            if quantity < booked_quantity:
                flash('Cannot reduce quantity below the amount currently booked')
                return redirect(url_for('edit_equipment', equipment_id=equipment_id))

            # Update equipment
            equipment['name'] = name
            # Calculate new available quantity
            equipment['available'] += (quantity - equipment['quantity'])
            equipment['quantity'] = quantity

            save_equipment(equipment_list)

            flash('Equipment updated successfully')
            return redirect(url_for('index'))

        except ValueError:
            flash('Please enter a valid quantity')
            return redirect(url_for('edit_equipment', equipment_id=equipment_id))

    # GET request - show edit equipment form
    template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>Edit Equipment</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                line-height: 1.6;
                margin: 0;
                padding: 20px;
                background-color: #f4f4f4;
            }
            .container {
                max-width: 700px;
                margin: 0 auto;
                background: white;
                padding: 20px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            .header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 20px;
                padding-bottom: 10px;
                border-bottom: 1px solid #ddd;
            }
            .user-info {
                text-align: right;
            }
            .btn {
                display: inline-block;
                padding: 8px 16px;
                background: #4CAF50;
                color: white;
                text-decoration: none;
                border-radius: 4px;
                border: none;
                cursor: pointer;
            }
            .btn-secondary {
                background: #6c757d;
            }
            .btn-logout {
                background: #dc3545;
                padding: 6px 12px;
                margin-left: 10px;
            }
            .btn-info {
                background: #17a2b8;
            }
            .btn-primary {
                background: #007bff;
            }
            .nav {
                display: flex;
                gap: 10px;
                margin-bottom: 20px;
            }
            .form-group {
                margin-bottom: 15px;
            }
            label {
                display: block;
                margin-bottom: 5px;
                font-weight: bold;
            }
            input {
                width: 100%;
                padding: 8px;
                border: 1px solid #ddd;
                border-radius: 4px;
                box-sizing: border-box;
            }
            .flash {
                padding: 10px;
                margin-bottom: 15px;
                background-color: #d4edda;
                color: #155724;
                border-radius: 4px;
            }
            .equipment-details {
                background-color: #f8f9fa;
                padding: 15px;
                border-radius: 4px;
                margin-bottom: 20px;
            }
            .warning {
                color: #856404;
                background-color: #fff3cd;
                padding: 10px;
                border-radius: 4px;
                margin-bottom: 15px;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header">
                <h1>Edit Equipment</h1>
                <div class="user-info">
                    Logged in as: <strong>{{ session.username }}</strong> (Manager)
                    <a href="{{ url_for('logout') }}" class="btn btn-logout">Logout</a>
                </div>
            </div>

            <div class="nav">
                <a href="{{ url_for('index') }}" class="btn btn-primary">Equipment</a>
                <a href="{{ url_for('my_bookings') }}" class="btn btn-info">My Bookings</a>
                <a href="{{ url_for('all_bookings') }}" class="btn btn-info">All Bookings</a>
                <a href="{{ url_for('add_equipment') }}" class="btn">Add Equipment</a>
            </div>

            {% if get_flashed_messages() %}
            <div class="flash">
                {{ get_flashed_messages()[0] }}
            </div>
            {% endif %}

            <div class="equipment-details">
                <h3>Current Details</h3>
                <p><strong>Name:</strong> {{ equipment.name }}</p>
                <p><strong>Total Quantity:</strong> {{ equipment.quantity }}</p>
                <p><strong>Available:</strong> {{ equipment.available }}</p>
                <p><strong>Currently Booked:</strong> {{ equipment.quantity - equipment.available }}</p>
            </div>

            {% if equipment.quantity > equipment.available %}
            <div class="warning">
                <strong>Note:</strong> Some of this equipment is currently booked. You cannot reduce the total quantity below {{ equipment.quantity - equipment.available }}.
            </div>
            {% endif %}

            <form method="post">
                <div class="form-group">
                    <label for="name">Equipment Name:</label>
                    <input type="text" id="name" name="name" value="{{ equipment.name }}" required>
                </div>

                <div class="form-group">
                    <label for="quantity">Total Quantity:</label>
                    <input type="number" id="quantity" name="quantity" min="{{ equipment.quantity - equipment.available }}" value="{{ equipment.quantity }}" required>
                </div>

                <div style="display: flex; gap: 10px;">
                    <button type="submit" class="btn">Update Equipment</button>
                    <a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
                </div>
            </form>
        </div>
    </body>
    </html>
    """

    return render_template_string(template, equipment=equipment)

if __name__ == '__main__':
    app.run(debug=True)



0
0
0.000
0 comments