Browse Source

Rewrite of everything

master
Alan Woodman 2 weeks ago
parent
commit
6357c84a62
  1. 18
      app.py
  2. 164
      blueprints/main.py
  3. 6
      config.py
  4. 8
      models.py
  5. 2
      payment_processors/batch_processor.py
  6. 815
      static/css/custom.css
  7. 693
      templates/analytics/dashboard.html
  8. 164
      templates/auth/add_user.html
  9. 93
      templates/auth/list_users.html
  10. 120
      templates/auth/login.html
  11. 242
      templates/base.html
  12. 195
      templates/base_original.html
  13. 627
      templates/hades/base.html
  14. 2161
      templates/hades/location_ont_details.html
  15. 61
      templates/main/add_payment_method.html
  16. 1219
      templates/main/batch_detail.html
  17. 121
      templates/main/batch_list.html
  18. 522
      templates/main/batch_payment_detail.html
  19. 24
      templates/main/index.html
  20. 477
      templates/main/logs_list.html
  21. 1139
      templates/main/payment_detail.html
  22. 61
      templates/main/payment_plans_form.html
  23. 346
      templates/main/payment_plans_list.html
  24. 585
      templates/main/single_payment.html
  25. 1185
      templates/main/single_payment_detail.html
  26. 1029
      templates/main/single_payments_list.html
  27. 227
      templates/search/search.html

18
app.py

@ -4,6 +4,7 @@ from flask_migrate import Migrate
from flask_login import LoginManager
import pymysql
import os
import json
from config import Config
db = SQLAlchemy()
@ -85,6 +86,23 @@ def create_app():
'get_user_permission_level': get_user_permission_level
}
# Add custom Jinja2 filter for JSON formatting
@app.template_filter('format_json')
def format_json_filter(value):
"""Format JSON string with proper indentation."""
if not value:
return ''
try:
# If it's already a dict/list, just dump it
if isinstance(value, (dict, list)):
return json.dumps(value, indent=2)
# If it's a string, parse and re-dump with formatting
parsed = json.loads(value)
return json.dumps(parsed, indent=2)
except (json.JSONDecodeError, TypeError):
# If it fails to parse, return original value
return value
# Note: Database tables will be managed by Flask-Migrate
# Use 'flask db init', 'flask db migrate', 'flask db upgrade' commands

164
blueprints/main.py

@ -1,9 +1,11 @@
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
from flask_login import login_required, current_user
from sqlalchemy import func, case
import json
import pymysql
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app
from flask_login import login_required, current_user
from sqlalchemy import func, case
from datetime import datetime
from app import db
from typing import Dict, Any, List
from models import PaymentBatch, Payments, SinglePayments, PaymentPlans, Logs, Users
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from stripe_payment_processor import StripePaymentProcessor
@ -307,50 +309,55 @@ def processPaymentResult(pay_id, result, key):
print(f"processPaymentResult error: {e}\n{json.dumps(result)}")
payment.PI_FollowUp = True
def find_pay_splynx_invoices(splynx_id):
def find_pay_splynx_invoices(splynx_id: int, splynx_pay_id: int, invoice_ids: List[int]) -> List[int]:
"""Mark Splynx invoices as paid for the given customer ID."""
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid")
params = {
'main_attributes': {
'customer_id': splynx_id,
'status': ['IN', ['not_paid', 'pending']]
},
}
query_string = splynx.build_splynx_query_params(params)
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
print(f"\n\nInvoice IDs to Pay: {invoice_ids} of type {type(invoice_ids)}\n")
#params = {
# 'main_attributes': {
# 'customer_id': splynx_id,
# 'status': ['IN', ['not_paid', 'pending']]
# },
#}
#query_string = splynx.build_splynx_query_params(params)
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pay = {
"status": "paid"
"status": "paid",
"payment_id": splynx_pay_id,
"date_payment": datetime.now().strftime("%Y-%m-%d")
}
for pay in result:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay)
return res
#for pay in result:
for invoice in invoice_ids:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pay)
return res
def find_set_pending_splynx_invoices(splynx_id):
def find_set_pending_splynx_invoices(splynx_id: int, invoice_list: List):
"""Mark Splynx invoices as pending for the given customer ID."""
params = {
'main_attributes': {
'customer_id': splynx_id,
'status': 'not_paid'
},
}
query_string = splynx.build_splynx_query_params(params)
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
#params = {
# 'main_attributes': {
# 'customer_id': splynx_id,
# 'status': 'not_paid'
# },
#}
#query_string = splynx.build_splynx_query_params(params)
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pending = {
"status": "pending"
}
updated_invoices = []
for invoice in result:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pending)
for invoice in invoice_list:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pending)
if res:
updated_invoices.append(res)
return updated_invoices
def add_payment_splynx(splynx_id, pi_id, pay_id, amount):
def add_payment_splynx(splynx_id, pi_id, pay_id, amount, invoice_id):
"""Add a payment record to Splynx."""
from datetime import datetime
@ -359,7 +366,8 @@ def add_payment_splynx(splynx_id, pi_id, pay_id, amount):
"amount": amount,
"date": str(datetime.now().strftime('%Y-%m-%d')),
"field_1": pi_id,
"field_2": f"Single Payment_ID: {pay_id}"
"field_2": f"Single Payment_ID: {pay_id}",
"invoice_id": invoice_id
}
res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay)
@ -751,7 +759,8 @@ def payment_detail(payment_id):
payment = db.session.query(Payments).filter(Payments.id == payment_id).first()
if not payment:
flash('Payment not found.', 'error')
current_app.logger.warning(f"Payment ID {payment_id} not found in Payments table")
flash(f'Payment #{payment_id} not found in batch payments.', 'error')
return redirect(url_for('main.batch_list'))
# Log the payment detail view access
@ -763,7 +772,12 @@ def payment_detail(payment_id):
details=f"Viewed batch payment detail for payment ID {payment_id}"
)
return render_template('main/payment_detail.html', payment=payment)
# Batch payments don't store individual user info, so set to Batch Processor
payment.processed_by = 'Batch Processor'
# Render the batch payment detail template
current_app.logger.info(f"Rendering batch payment detail for payment {payment_id} (batch {payment.PaymentBatch_ID})")
return render_template('main/batch_payment_detail.html', payment=payment)
@main_bp.route('/single-payment/check-intent/<int:payment_id>', methods=['POST'])
@login_required
@ -814,6 +828,37 @@ def check_payment_intent(payment_id):
print(f"Check payment intent error: {e}")
return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500
@main_bp.route('/api/splynx/invoices/<int:splynx_id>', methods=['GET'])
@helpdesk_required
def get_customer_invoices(splynx_id):
"""Fetch unpaid invoices for a customer from Splynx."""
try:
params = {
'main_attributes': {
'customer_id': splynx_id,
'status': 'not_paid'
}
}
query_string = splynx.build_splynx_query_params(params)
invoices = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
# Format invoice data for frontend
formatted_invoices = []
for inv in invoices:
formatted_invoices.append({
'id': inv['id'],
'number': inv.get('number', 'N/A'),
'date': inv.get('date', 'N/A'),
'total': float(inv.get('total', 0)),
'status': inv.get('status', 'unknown'),
'description': inv.get('memo', 'No description')
})
return jsonify({'success': True, 'invoices': formatted_invoices})
except Exception as e:
print(f"Error fetching invoices: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@main_bp.route('/single-payment/process', methods=['POST'])
@helpdesk_required
def process_single_payment():
@ -823,6 +868,8 @@ def process_single_payment():
splynx_id = request.form.get('splynx_id')
amount = request.form.get('amount')
payment_method = request.form.get('payment_method')
invoice_ids = request.form.get('invoice_ids', [])
invoice_list = invoice_ids.split(",") if invoice_ids else [0]
# Validate inputs
if not splynx_id or not amount or not payment_method:
@ -852,7 +899,8 @@ def process_single_payment():
Splynx_ID=splynx_id,
Stripe_Customer_ID=stripe_customer_id,
Payment_Amount=amount,
Who=current_user.id
Who=current_user.id,
Invoices_to_Pay=invoice_ids
)
db.session.add(payment_record)
db.session.commit() # Commit to get the payment ID
@ -982,18 +1030,20 @@ def process_single_payment():
if result.get('needs_fee_update'):
payment_record.PI_FollowUp = True
# Mark invoices as pending when PI_FollowUp is set
if Config.PROCESS_LIVE:
try:
find_set_pending_splynx_invoices(splynx_id)
except Exception as e:
print(f"⚠️ Error setting invoices to pending: {e}")
#if Config.PROCESS_LIVE:
try:
pending_invoices = find_set_pending_splynx_invoices(splynx_id, invoice_list)
if invoice_list[0] == 0:
payment_record.Invoices_to_Pay = ','.join(pending_invoices)
except Exception as e:
print(f"⚠️ Error setting invoices to pending: {e}")
if result.get('payment_method_type') == "card":
payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card')
elif result.get('payment_method_type') == "au_becs_debit":
payment_record.Payment_Method = result['payment_method_type']
if result.get('fee_details'):
if result.get('fee_details') and result.get('fee_details').get('fee_breakdown'):
payment_record.Fee_Total = result['fee_details']['total_fee']
for fee_type in result['fee_details']['fee_breakdown']:
if fee_type['type'] == "tax":
@ -1007,27 +1057,27 @@ def process_single_payment():
# Check if payment was actually successful
if result.get('success'):
# Payment succeeded - update Splynx if in live mode
if Config.PROCESS_LIVE:
try:
# Mark invoices as paid in Splynx
find_pay_splynx_invoices(splynx_id)
# Add payment record to Splynx
splynx_payment_id = add_payment_splynx(
splynx_id=splynx_id,
pi_id=result.get('payment_intent_id'),
pay_id=payment_record.id,
amount=amount
)
#if Config.PROCESS_LIVE:
try:
# Add payment record to Splynx
splynx_payment_id = add_payment_splynx(
splynx_id=splynx_id,
pi_id=result.get('payment_intent_id'),
pay_id=payment_record.id,
amount=amount,
invoice_id=invoice_list[0]
)
if splynx_payment_id:
print(f"✅ Splynx payment record created: {splynx_payment_id}")
else:
print("⚠️ Failed to create Splynx payment record")
if splynx_payment_id:
print(f"✅ Splynx payment record created: {splynx_payment_id}")
# Mark invoices as paid in Splynx
find_pay_splynx_invoices(splynx_id, splynx_payment_id, invoice_list)
else:
print("⚠️ Failed to create Splynx payment record")
except Exception as splynx_error:
print(f"❌ Error updating Splynx: {splynx_error}")
# Continue processing even if Splynx update fails
except Exception as splynx_error:
print(f"❌ Error updating Splynx: {splynx_error}")
# Continue processing even if Splynx update fails
# Log successful payment
log_activity(

6
config.py

@ -5,8 +5,8 @@ class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'plutus-dev-secret-key-change-in-production'
# PostgreSQL database configuration (Flask-SQLAlchemy)
#SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus'
SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus'
SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus'
#SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus'
SQLALCHEMY_TRACK_MODIFICATIONS = False
@ -37,7 +37,7 @@ class Config:
# Threading configuration
MAX_PAYMENT_THREADS = 15 # Number of concurrent payment processing threads
THREAD_TIMEOUT = 60 # Timeout in seconds for payment processing threads
THREAD_TIMEOUT = 30 # Timeout in seconds for payment processing threads
# Stripe API Keys
STRIPE_LIVE_API_KEY = os.environ.get('STRIPE_LIVE_API_KEY') or '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'

8
models.py

@ -30,7 +30,7 @@ class Users(UserMixin, db.Model):
class PaymentBatch(db.Model):
__tablename__ = 'PaymentBatch'
id = db.Column(db.Integer, primary_key=True)
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class Payments(db.Model):
@ -58,7 +58,7 @@ class Payments(db.Model):
Refund_JSON = db.Column(db.Text())
Stripe_Refund_ID = db.Column(db.String())
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True)
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
PaymentPlan_ID = db.Column(db.Integer, db.ForeignKey('PaymentPlans.id'), nullable=True)
Invoices_to_Pay = db.Column(db.String())
@ -86,7 +86,7 @@ class SinglePayments(db.Model):
Refund_JSON = db.Column(db.Text())
Stripe_Refund_ID = db.Column(db.String())
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True)
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False)
Invoices_to_Pay = db.Column(db.String())
@ -111,6 +111,6 @@ class PaymentPlans(db.Model):
Frequency = db.Column(db.String(50))
Start_Date = db.Column(db.DateTime, nullable=True)
Stripe_Payment_Method = db.Column(db.String(50))
Created = db.Column(db.DateTime, nullable=False, default=datetime.now())
Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False)
Enabled = db.Column(db.Boolean, nullable=True, default=True)

2
payment_processors/batch_processor.py

@ -152,7 +152,7 @@ class BatchPaymentProcessor(BasePaymentProcessor):
else:
#self.logger.info(f"No customers found for {payment_method_names[pm]}")
self.logger.info(f"No customers found for {str(payment_methods)}")
sys.exit()
#sys.exit()
return batch_ids
def _execute_payment_batches(self, batch_ids: List[int]) -> tuple:

815
static/css/custom.css

File diff suppressed because it is too large

693
templates/analytics/dashboard.html

@ -4,6 +4,58 @@
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.metric-card {
background: white;
border-radius: 8px;
@ -51,7 +103,7 @@
.log-entry {
background: #f8f9fa;
border-left: 4px solid #3273dc;
border-left: 4px solid #0d6efd;
padding: 12px;
margin-bottom: 10px;
border-radius: 4px;
@ -61,294 +113,203 @@
opacity: 0.6;
pointer-events: none;
}
/* Tab styling improvements */
.tabs ul {
border-bottom: 2px solid #dbdbdb;
}
.tabs li {
background-color: #f5f5f5;
border: 1px solid #dbdbdb;
border-bottom: none;
margin-right: 2px;
}
.tabs li:hover {
background-color: #e8e8e8;
}
.tabs li.is-active {
background-color: #ffffff;
border-color: #3273dc;
border-bottom: 2px solid #ffffff;
margin-bottom: -2px;
position: relative;
z-index: 1;
}
.tabs li a {
color: #4a4a4a;
font-weight: 500;
padding: 0.75em 1em;
border: none;
background: transparent;
}
.tabs li.is-active a {
color: #3273dc;
font-weight: 600;
}
.tabs li:hover a {
color: #363636;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% endblock %}
{% block content %}
<section class="section">
<div class="container">
<div class="level">
<div class="level-left">
<div class="level-item">
<h1 class="title">
<span class="icon">
<i class="fas fa-chart-line"></i>
</span>
Analytics Dashboard
</h1>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="field is-grouped">
<p class="control">
<button class="button is-light" onclick="refreshDashboard()">
<span class="icon">
<i class="fas fa-sync-alt"></i>
</span>
<span>Refresh</span>
</button>
</p>
<p class="control">
<button class="button is-primary" onclick="exportReport()">
<span class="icon">
<i class="fas fa-download"></i>
</span>
<span>Export</span>
</button>
</p>
</div>
</div>
</div>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="h2 mb-1">
<i class="fas fa-chart-line"></i> Analytics Dashboard
</h1>
</div>
<div class="d-flex gap-2">
<button class="btn btn-light" onclick="refreshDashboard()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button class="btn btn-primary" onclick="exportReport()">
<i class="fas fa-download"></i> Export
</button>
</div>
</div>
<!-- System Health Overview -->
<div class="columns">
<div class="column is-3">
<div class="metric-card">
<div id="healthScore" class="health-score">
<span id="healthValue">--</span>
</div>
<div class="metric-label">System Health Score</div>
<p class="is-size-7 has-text-grey">Overall system performance</p>
</div>
</div>
<div class="column is-3">
<div class="metric-card">
<div id="paymentSuccessRate" class="metric-value has-text-success">--%</div>
<div class="metric-label">Payment Success Rate</div>
<p class="is-size-7 has-text-grey">Last 24 hours</p>
<!-- System Health Overview -->
<div class="row">
<div class="col-md-3">
<div class="metric-card">
<div id="healthScore" class="health-score">
<span id="healthValue">--</span>
</div>
<div class="metric-label">System Health Score</div>
<p class="small text-muted">Overall system performance</p>
</div>
<div class="column is-3">
<div class="metric-card">
<div id="errorRate" class="metric-value has-text-warning">--%</div>
<div class="metric-label">Error Rate</div>
<p class="is-size-7 has-text-grey">System errors in logs</p>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div id="paymentSuccessRate" class="metric-value text-success">--%</div>
<div class="metric-label">Payment Success Rate</div>
<p class="small text-muted">Last 24 hours</p>
</div>
<div class="column is-3">
<div class="metric-card">
<div id="totalPayments" class="metric-value has-text-info">--</div>
<div class="metric-label">Total Payments</div>
<p class="is-size-7 has-text-grey">Recent activity</p>
</div>
</div>
<div class="col-md-3">
<div class="metric-card">
<div id="errorRate" class="metric-value text-warning">--%</div>
<div class="metric-label">Error Rate</div>
<p class="small text-muted">System errors in logs</p>
</div>
</div>
<!-- Tabs -->
<div class="tabs is-centered">
<ul>
<li class="is-active" data-tab="performance">
<a>
<span class="icon is-small"><i class="fas fa-tachometer-alt"></i></span>
<span>Performance</span>
</a>
</li>
<li data-tab="payments">
<a>
<span class="icon is-small"><i class="fas fa-credit-card"></i></span>
<span>Payments</span>
</a>
</li>
<li data-tab="security">
<a>
<span class="icon is-small"><i class="fas fa-shield-alt"></i></span>
<span>Security</span>
</a>
</li>
<li data-tab="logs">
<a>
<span class="icon is-small"><i class="fas fa-list-alt"></i></span>
<span>Logs</span>
</a>
</li>
</ul>
<div class="col-md-3">
<div class="metric-card">
<div id="totalPayments" class="metric-value text-info">--</div>
<div class="metric-label">Total Payments</div>
<p class="small text-muted">Recent activity</p>
</div>
</div>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Performance Tab -->
<div id="performance-content" class="tab-pane is-active">
<div class="box">
<h4 class="title is-4">
<span class="icon">
<i class="fas fa-clock"></i>
</span>
System Performance
<!-- Tabs -->
<ul class="nav nav-tabs justify-content-center" id="analyticsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance-content" type="button" role="tab" data-tab="performance">
<i class="fas fa-tachometer-alt"></i> Performance
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="payments-tab" data-bs-toggle="tab" data-bs-target="#payments-content" type="button" role="tab" data-tab="payments">
<i class="fas fa-credit-card"></i> Payments
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="security-tab" data-bs-toggle="tab" data-bs-target="#security-content" type="button" role="tab" data-tab="security">
<i class="fas fa-shield-alt"></i> Security
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs-content" type="button" role="tab" data-tab="logs">
<i class="fas fa-list-alt"></i> Logs
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content">
<!-- Performance Tab -->
<div class="tab-pane fade show active" id="performance-content" role="tabpanel">
<div class="card shadow mt-3">
<div class="card-body">
<h4 class="h5 mb-3">
<i class="fas fa-clock"></i> System Performance
</h4>
<div id="performanceMetrics">
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading performance metrics...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading performance metrics...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Payments Tab -->
<div id="payments-content" class="tab-pane" style="display: none;">
<div class="box">
<h4 class="title is-4">
<span class="icon">
<i class="fas fa-chart-line"></i>
</span>
Payment Analytics
<!-- Payments Tab -->
<div class="tab-pane fade" id="payments-content" role="tabpanel">
<div class="card shadow mt-3">
<div class="card-body">
<h4 class="h5 mb-3">
<i class="fas fa-chart-line"></i> Payment Analytics
</h4>
<div id="paymentMetrics">
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading payment analytics...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading payment analytics...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Security Tab -->
<div id="security-content" class="tab-pane" style="display: none;">
<div class="columns">
<div class="column is-4">
<div class="metric-card">
<div id="securityEvents" class="metric-value has-text-success">--</div>
<div class="metric-label">Security Events</div>
<p class="is-size-7 has-text-grey">Last 7 days</p>
</div>
<!-- Security Tab -->
<div class="tab-pane fade" id="security-content" role="tabpanel">
<div class="row mt-3">
<div class="col-md-4">
<div class="metric-card">
<div id="securityEvents" class="metric-value text-success">--</div>
<div class="metric-label">Security Events</div>
<p class="small text-muted">Last 7 days</p>
</div>
<div class="column is-4">
<div class="metric-card">
<div id="failedLogins" class="metric-value has-text-warning">--</div>
<div class="metric-label">Failed Logins</div>
<p class="is-size-7 has-text-grey">Authentication failures</p>
</div>
</div>
<div class="col-md-4">
<div class="metric-card">
<div id="failedLogins" class="metric-value text-warning">--</div>
<div class="metric-label">Failed Logins</div>
<p class="small text-muted">Authentication failures</p>
</div>
<div class="column is-4">
<div class="metric-card">
<div id="blockedRequests" class="metric-value has-text-danger">--</div>
<div class="metric-label">Blocked Requests</div>
<p class="is-size-7 has-text-grey">Suspicious activity</p>
</div>
</div>
<div class="col-md-4">
<div class="metric-card">
<div id="blockedRequests" class="metric-value text-danger">--</div>
<div class="metric-label">Blocked Requests</div>
<p class="small text-muted">Suspicious activity</p>
</div>
</div>
<div class="box">
<h4 class="title is-4">
<span class="icon">
<i class="fas fa-shield-alt"></i>
</span>
Security Events
</div>
<div class="card shadow">
<div class="card-body">
<h4 class="h5 mb-3">
<i class="fas fa-shield-alt"></i> Security Events
</h4>
<div id="securityEventsList">
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading security events...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading security events...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Logs Tab -->
<div id="logs-content" class="tab-pane" style="display: none;">
<div class="box">
<div class="level">
<div class="level-left">
<div class="level-item">
<h4 class="title is-4">
<span class="icon">
<i class="fas fa-search"></i>
</span>
Log Search
</h4>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="field has-addons">
<div class="control">
<input type="text" id="logSearch" class="input" placeholder="Search logs...">
</div>
<div class="control">
<div class="select">
<select id="logAction">
<option value="">All Actions</option>
<option value="LOGIN_SUCCESS">Login Success</option>
<option value="LOGIN_FAILED">Login Failed</option>
<option value="PAYMENT_PROCESSED">Payment Processed</option>
<option value="BATCH_CREATED">Batch Created</option>
</select>
</div>
</div>
<div class="control">
<button class="button is-primary" onclick="searchLogs()">
<span class="icon">
<i class="fas fa-search"></i>
</span>
</button>
</div>
</div>
</div>
<!-- Logs Tab -->
<div class="tab-pane fade" id="logs-content" role="tabpanel">
<div class="card shadow mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="h5 mb-0">
<i class="fas fa-search"></i> Log Search
</h4>
<div class="d-flex gap-2">
<input type="text" id="logSearch" class="form-control form-control-sm" placeholder="Search logs..." style="max-width: 200px;">
<select id="logAction" class="form-select form-select-sm" style="max-width: 200px;">
<option value="">All Actions</option>
<option value="LOGIN_SUCCESS">Login Success</option>
<option value="LOGIN_FAILED">Login Failed</option>
<option value="PAYMENT_PROCESSED">Payment Processed</option>
<option value="BATCH_CREATED">Batch Created</option>
</select>
<button class="btn btn-sm btn-primary" onclick="searchLogs()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div id="logResults">
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading recent logs...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading recent logs...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<script>
$(document).ready(function() {
@ -359,18 +320,8 @@ $(document).ready(function() {
loadTabContent('performance');
// Tab switching
$('.tabs li').click(function() {
var tab = $(this).data('tab');
// Update tab appearance
$('.tabs li').removeClass('is-active');
$(this).addClass('is-active');
// Show/hide content
$('.tab-pane').hide().removeClass('is-active');
$('#' + tab + '-content').show().addClass('is-active');
// Load tab content
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
const tab = $(e.target).data('tab');
loadTabContent(tab);
});
@ -442,11 +393,11 @@ function loadTabContent(tab) {
function loadPerformanceMetrics() {
console.log('Loading performance metrics...');
$('#performanceMetrics').html(`
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading performance metrics...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading performance metrics...</p>
</div>
`);
@ -473,10 +424,9 @@ function loadPerformanceMetrics() {
console.log('Performance metrics data:', data);
if (data.error) {
$('#performanceMetrics').html(`
<div class="notification is-info">
<h5 class="title is-5">
<span class="icon"><i class="fas fa-chart-line"></i></span>
Performance Monitoring
<div class="alert alert-info">
<h5 class="h5">
<i class="fas fa-chart-line"></i> Performance Monitoring
</h5>
<p><strong>Status:</strong> ${data.message || data.error}</p>
<p>The system is actively collecting performance data. Check back after some activity.</p>
@ -486,94 +436,104 @@ function loadPerformanceMetrics() {
return;
}
let html = '<div class="content">';
let html = '<div>';
// System overview
if (data.system_info) {
html += '<div class="columns">';
html += '<div class="column is-4">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Monitoring Status</p>';
html += `<p class="title ${data.system_info.monitoring_active ? 'has-text-success' : 'has-text-warning'}">`;
html += `<span class="icon"><i class="fas ${data.system_info.monitoring_active ? 'fa-check-circle' : 'fa-exclamation-triangle'}"></i></span>`;
html += '<div class="row">';
html += '<div class="col-md-4">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Monitoring Status</p>';
html += `<p class="h4 ${data.system_info.monitoring_active ? 'text-success' : 'text-warning'}">`;
html += `<i class="fas ${data.system_info.monitoring_active ? 'fa-check-circle' : 'fa-exclamation-triangle'}"></i> `;
html += `${data.system_info.monitoring_active ? 'Active' : 'Inactive'}`;
html += '</p>';
html += '</div></div>';
html += '<div class="column is-4">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Log Files</p>';
html += `<p class="title has-text-info">${data.system_info.log_files_found || 0}</p>`;
html += '</div></div>';
html += '<div class="column is-4">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Collection Period</p>';
html += `<p class="title has-text-grey">${data.system_info.data_collection_period || '7 days'}</p>`;
html += '</div></div>';
html += '</div></div></div>';
html += '<div class="col-md-4">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Log Files</p>';
html += `<p class="h4 text-info">${data.system_info.log_files_found || 0}</p>`;
html += '</div></div></div>';
html += '<div class="col-md-4">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Collection Period</p>';
html += `<p class="h4 text-muted">${data.system_info.data_collection_period || '7 days'}</p>`;
html += '</div></div></div>';
html += '</div>';
}
// Performance summary
if (data.summary) {
html += '<h5 class="title is-5">Performance Summary</h5>';
html += '<div class="columns">';
html += '<div class="column is-3">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Slow Requests</p>';
html += `<p class="title ${data.summary.slow_request_count > 0 ? 'has-text-warning' : 'has-text-success'}">${data.summary.slow_request_count || 0}</p>`;
html += '<p class="help">Requests > 1 second</p>';
html += '</div></div>';
html += '<div class="column is-3">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Slow Queries</p>';
html += `<p class="title ${data.summary.database_queries > 0 ? 'has-text-warning' : 'has-text-success'}">${data.summary.database_queries || 0}</p>`;
html += '<p class="help">DB queries > 100ms</p>';
html += '</div></div>';
html += '<div class="column is-3">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Total Requests</p>';
html += `<p class="title has-text-info">${data.summary.total_requests || 'N/A'}</p>`;
html += '<p class="help">Recent requests</p>';
html += '</div></div>';
html += '<div class="column is-3">';
html += '<div class="box has-text-centered">';
html += '<p class="heading">Avg Response</p>';
html += `<p class="title has-text-info">${data.summary.avg_response_time || 'N/A'}</p>`;
html += '<p class="help">Milliseconds</p>';
html += '</div></div>';
html += '<h5 class="h5 mt-4">Performance Summary</h5>';
html += '<div class="row">';
html += '<div class="col-md-3">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Slow Requests</p>';
html += `<p class="h4 ${data.summary.slow_request_count > 0 ? 'text-warning' : 'text-success'}">${data.summary.slow_request_count || 0}</p>`;
html += '<p class="small text-muted">Requests > 1 second</p>';
html += '</div></div></div>';
html += '<div class="col-md-3">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Slow Queries</p>';
html += `<p class="h4 ${data.summary.database_queries > 0 ? 'text-warning' : 'text-success'}">${data.summary.database_queries || 0}</p>`;
html += '<p class="small text-muted">DB queries > 100ms</p>';
html += '</div></div></div>';
html += '<div class="col-md-3">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Total Requests</p>';
html += `<p class="h4 text-info">${data.summary.total_requests || 'N/A'}</p>`;
html += '<p class="small text-muted">Recent requests</p>';
html += '</div></div></div>';
html += '<div class="col-md-3">';
html += '<div class="card text-center">';
html += '<div class="card-body">';
html += '<p class="text-muted text-uppercase small">Avg Response</p>';
html += `<p class="h4 text-info">${data.summary.avg_response_time || 'N/A'}</p>`;
html += '<p class="small text-muted">Milliseconds</p>';
html += '</div></div></div>';
html += '</div>';
}
// Slow requests table
if (data.slow_requests && data.slow_requests.length > 0) {
html += '<h5 class="title is-5">Recent Slow Requests</h5>';
html += '<table class="table is-striped is-fullwidth">';
html += '<h5 class="h5 mt-4">Recent Slow Requests</h5>';
html += '<div class="table-responsive">';
html += '<table class="table table-striped">';
html += '<thead><tr><th>Time</th><th>Endpoint</th><th>Duration</th><th>Status</th></tr></thead>';
html += '<tbody>';
data.slow_requests.slice(0, 10).forEach(req => {
html += '<tr>';
html += `<td>${new Date(req.timestamp).toLocaleString()}</td>`;
html += `<td><code>${req.endpoint}</code></td>`;
html += `<td><span class="tag is-warning">${Math.round(req.duration_ms)}ms</span></td>`;
html += `<td><span class="tag ${req.status_code < 400 ? 'is-success' : 'is-danger'}">${req.status_code}</span></td>`;
html += `<td><span class="badge bg-warning">${Math.round(req.duration_ms)}ms</span></td>`;
html += `<td><span class="badge ${req.status_code < 400 ? 'bg-success' : 'bg-danger'}">${req.status_code}</span></td>`;
html += '</tr>';
});
html += '</tbody></table>';
html += '</div>';
} else {
html += '<div class="notification is-success">';
html += '<span class="icon"><i class="fas fa-check"></i></span>';
html += '<div class="alert alert-success mt-3">';
html += '<i class="fas fa-check"></i> ';
html += '<strong>Good Performance!</strong> No slow requests detected recently.';
html += '</div>';
}
// Slow queries table
if (data.slow_queries && data.slow_queries.length > 0) {
html += '<h5 class="title is-5">Recent Slow Database Queries</h5>';
html += '<table class="table is-striped is-fullwidth">';
html += '<h5 class="h5 mt-4">Recent Slow Database Queries</h5>';
html += '<div class="table-responsive">';
html += '<table class="table table-striped">';
html += '<thead><tr><th>Time</th><th>Table</th><th>Type</th><th>Duration</th></tr></thead>';
html += '<tbody>';
data.slow_queries.slice(0, 10).forEach(query => {
@ -581,10 +541,11 @@ function loadPerformanceMetrics() {
html += `<td>${new Date(query.timestamp).toLocaleString()}</td>`;
html += `<td><code>${query.table}</code></td>`;
html += `<td>${query.query_type}</td>`;
html += `<td><span class="tag is-warning">${Math.round(query.duration_ms)}ms</span></td>`;
html += `<td><span class="badge bg-warning">${Math.round(query.duration_ms)}ms</span></td>`;
html += '</tr>';
});
html += '</tbody></table>';
html += '</div>';
}
html += '</div>';
@ -602,12 +563,12 @@ function loadPerformanceMetrics() {
}
$('#performanceMetrics').html(`
<div class="notification is-warning">
<h5 class="title is-5">Performance Data Loading Error</h5>
<div class="alert alert-warning">
<h5 class="h5">Performance Data Loading Error</h5>
<p><strong>Error:</strong> ${errorMessage}</p>
<p>The system may be initializing or there may be a connectivity issue.</p>
<button class="button is-small is-primary" onclick="loadPerformanceMetrics()">Try Again</button>
<details style="margin-top: 10px;">
<button class="btn btn-sm btn-primary" onclick="loadPerformanceMetrics()">Try Again</button>
<details class="mt-2">
<summary>Debug Information</summary>
<pre>${error.stack || error.message}</pre>
</details>
@ -618,11 +579,11 @@ function loadPerformanceMetrics() {
function loadPaymentAnalytics() {
$('#paymentMetrics').html(`
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading payment analytics...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading payment analytics...</p>
</div>
`);
@ -631,7 +592,7 @@ function loadPaymentAnalytics() {
.then(data => {
if (data.error) {
$('#paymentMetrics').html(`
<div class="notification is-info">
<div class="alert alert-info">
<p><strong>Payment analytics:</strong> No recent payment data available.</p>
<p>Analytics will appear after payment processing activity.</p>
</div>
@ -639,7 +600,7 @@ function loadPaymentAnalytics() {
return;
}
let html = '<div class="content">';
let html = '<div>';
html += '<h5>Payment Analytics Overview</h5>';
html += '<p>Payment analytics are being collected. Detailed metrics will appear with payment activity.</p>';
html += '</div>';
@ -649,7 +610,7 @@ function loadPaymentAnalytics() {
.catch(error => {
console.error('Error loading payment analytics:', error);
$('#paymentMetrics').html(`
<div class="notification is-info">
<div class="alert alert-info">
<p>Payment analytics will be available after payment processing activity.</p>
</div>
`);
@ -658,11 +619,11 @@ function loadPaymentAnalytics() {
function loadSecurityEvents() {
$('#securityEventsList').html(`
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Loading security events...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading security events...</p>
</div>
`);
@ -671,7 +632,7 @@ function loadSecurityEvents() {
.then(data => {
if (data.error) {
$('#securityEventsList').html(`
<div class="notification is-success">
<div class="alert alert-success">
<p><strong>Security status:</strong> All clear - no security events detected.</p>
<p>The system is actively monitoring for security threats.</p>
</div>
@ -688,10 +649,8 @@ function loadSecurityEvents() {
if (data.summary.total_events === 0) {
$('#securityEventsList').html(`
<div class="notification is-success">
<span class="icon">
<i class="fas fa-shield-alt"></i>
</span>
<div class="alert alert-success">
<i class="fas fa-shield-alt"></i>
<strong>All clear!</strong> No security events detected.
</div>
`);
@ -702,10 +661,8 @@ function loadSecurityEvents() {
.catch(error => {
console.error('Error loading security events:', error);
$('#securityEventsList').html(`
<div class="notification is-success">
<span class="icon">
<i class="fas fa-shield-alt"></i>
</span>
<div class="alert alert-success">
<i class="fas fa-shield-alt"></i>
Security monitoring is active.
</div>
`);
@ -720,11 +677,11 @@ function searchLogs() {
const action = $('#logAction').val();
$('#logResults').html(`
<div class="has-text-centered">
<span class="icon is-large">
<i class="fas fa-spinner fa-pulse"></i>
</span>
<p>Searching logs...</p>
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Searching logs...</p>
</div>
`);
@ -737,7 +694,7 @@ function searchLogs() {
.then(data => {
if (data.error) {
$('#logResults').html(`
<div class="notification is-warning">
<div class="alert alert-warning">
<p><strong>Log search error:</strong> ${data.error}</p>
</div>
`);
@ -746,7 +703,7 @@ function searchLogs() {
if (!data.logs || data.logs.length === 0) {
$('#logResults').html(`
<div class="notification is-info">
<div class="alert alert-info">
<p>No logs found matching your search criteria.</p>
</div>
`);
@ -755,25 +712,21 @@ function searchLogs() {
let html = '';
data.logs.forEach(log => {
const logClass = log.action.includes('ERROR') || log.action.includes('FAILED') ? 'has-background-danger-light' :
log.action.includes('WARNING') ? 'has-background-warning-light' :
log.action.includes('SUCCESS') ? 'has-background-success-light' : '';
const logClass = log.action.includes('ERROR') || log.action.includes('FAILED') ? 'bg-danger bg-opacity-10' :
log.action.includes('WARNING') ? 'bg-warning bg-opacity-10' :
log.action.includes('SUCCESS') ? 'bg-success bg-opacity-10' : '';
html += `<div class="log-entry ${logClass}">
<div class="level">
<div class="level-left">
<div class="level-item">
<strong>${log.action}</strong>
</div>
<div class="d-flex justify-content-between">
<div>
<strong>${log.action}</strong>
</div>
<div class="level-right">
<div class="level-item">
<small class="has-text-grey">${new Date(log.timestamp).toLocaleString()}</small>
</div>
<div>
<small class="text-muted">${new Date(log.timestamp).toLocaleString()}</small>
</div>
</div>
<div>${log.message || 'No message'}</div>
<small class="has-text-grey">
<small class="text-muted">
${log.entity_type} ${log.entity_id || ''} | User ID: ${log.user_id || 'System'} | IP: ${log.ip_address || 'Unknown'}
</small>
</div>`;
@ -784,7 +737,7 @@ function searchLogs() {
.catch(error => {
console.error('Error searching logs:', error);
$('#logResults').html(`
<div class="notification is-warning">
<div class="alert alert-warning">
<p>Error searching logs. Please try again.</p>
</div>
`);
@ -797,7 +750,8 @@ function refreshDashboard() {
refreshSystemHealth();
// Refresh current tab content
const activeTab = $('.tabs li.is-active').data('tab');
const activeTabButton = document.querySelector('.nav-link.active');
const activeTab = activeTabButton ? activeTabButton.getAttribute('data-tab') : null;
if (activeTab) {
loadTabContent(activeTab);
}
@ -808,7 +762,8 @@ function refreshDashboard() {
}
function exportReport() {
const activeTab = $('.tabs li.is-active').data('tab') || 'system';
const activeTabButton = document.querySelector('.nav-link.active');
const activeTab = activeTabButton ? activeTabButton.getAttribute('data-tab') : 'system';
fetch(`/analytics/api/generate-report?type=${activeTab}&days=7`)
.then(response => response.json())

164
templates/auth/add_user.html

@ -2,74 +2,120 @@
{% block title %}Add User - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container, .row {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<div class="columns is-centered">
<div class="column is-6">
<div class="box">
<h1 class="title">Add New User</h1>
<form method="POST">
<div class="field">
<label class="label">Username</label>
<div class="control has-icons-left">
<input class="input" type="text" name="username" placeholder="Username" required>
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-body">
<h1 class="h3 mb-4">Add New User</h1>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
</div>
</div>
</div>
<div class="field">
<label class="label">Full Name</label>
<div class="control has-icons-left">
<input class="input" type="text" name="full_name" placeholder="Full Name" required>
<span class="icon is-small is-left">
<i class="fas fa-id-card"></i>
</span>
<div class="mb-3">
<label for="full_name" class="form-label">Full Name</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-id-card"></i></span>
<input type="text" class="form-control" id="full_name" name="full_name" placeholder="Full Name" required>
</div>
</div>
</div>
<div class="field">
<label class="label">Email</label>
<div class="control has-icons-left">
<input class="input" type="email" name="email" placeholder="Email Address" required>
<span class="icon is-small is-left">
<i class="fas fa-envelope"></i>
</span>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input type="email" class="form-control" id="email" name="email" placeholder="Email Address" required>
</div>
</div>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control has-icons-left">
<input class="input" type="password" name="password" placeholder="Password" required>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
</div>
</div>
</div>
<div class="field">
<label class="label">Permissions</label>
<div class="control">
<input class="input" type="text" name="permissions" placeholder="Permissions (optional)">
<div class="mb-3">
<label for="permissions" class="form-label">Permissions</label>
<input type="text" class="form-control" id="permissions" name="permissions" placeholder="Permissions (optional)">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>Add User</span>
<div class="d-flex gap-2">
<button class="btn btn-primary" type="submit">
<i class="fas fa-plus"></i> Add User
</button>
<a class="btn btn-light" href="{{ url_for('auth.list_users') }}">Cancel</a>
</div>
<div class="control">
<a class="button is-light" href="{{ url_for('auth.list_users') }}">Cancel</a>
</div>
</div>
</form>
</form>
</div>
</div>
</div>
</div>

93
templates/auth/list_users.html

@ -2,24 +2,83 @@
{% block title %}Users - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.table-responsive {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<div class="level">
<div class="level-left">
<h1 class="title">Users</h1>
</div>
<div class="level-right">
<a class="button is-primary" href="{{ url_for('auth.add_user') }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>Add User</span>
</a>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0">Users</h1>
<a class="btn btn-primary" href="{{ url_for('auth.add_user') }}">
<i class="fas fa-plus"></i> Add User
</a>
</div>
{% if users %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
@ -39,7 +98,7 @@
<td>{{ user.FullName }}</td>
<td>{{ user.Email }}</td>
<td>
<span class="tag is-{{ 'success' if user.Enabled else 'danger' }}">
<span class="badge bg-{{ 'success' if user.Enabled else 'danger' }}">
{{ 'Active' if user.Enabled else 'Disabled' }}
</span>
</td>
@ -51,8 +110,8 @@
</table>
</div>
{% else %}
<div class="notification is-info">
<p>No users found. <a href="{{ url_for('auth.add_user') }}">Add the first user</a>.</p>
<div class="alert alert-info">
<p class="mb-0">No users found. <a href="{{ url_for('auth.add_user') }}" class="alert-link">Add the first user</a>.</p>
</div>
{% endif %}
{% endblock %}

120
templates/auth/login.html

@ -2,44 +2,98 @@
{% block title %}Login - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container, .row {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<div class="columns is-centered">
<div class="column is-4">
<div class="box">
<h1 class="title has-text-centered">Login to Plutus</h1>
<form method="POST">
<div class="field">
<label class="label">Username</label>
<div class="control has-icons-left">
<input class="input" type="text" name="username" placeholder="Username" required>
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow">
<div class="card-body">
<h1 class="h3 text-center mb-4">Login to Plutus</h1>
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-user"></i></span>
<input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
</div>
</div>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control has-icons-left">
<input class="input" type="password" name="password" placeholder="Password" required>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
</div>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-primary is-fullwidth" type="submit">
<span class="icon">
<i class="fas fa-sign-in-alt"></i>
</span>
<span>Login</span>
<div class="d-grid">
<button class="btn btn-primary" type="submit">
<i class="fas fa-sign-in-alt"></i> Login
</button>
</div>
</div>
</form>
</form>
</div>
</div>
</div>
</div>

242
templates/base.html

@ -4,165 +4,123 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Plutus{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar is-dark" role="navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{{ url_for('main.index') }}">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<strong>Plutus</strong>
</a>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-menu">
<div class="navbar-start">
{% if current_user.is_authenticated %}
<a class="navbar-item" href="{{ url_for('main.index') }}">
Dashboard
</a>
{% if can_view_data() %}
<a class="navbar-item" href="{{ url_for('search.search_page') }}">
<span class="icon">
<i class="fas fa-search"></i>
</span>
<span>Search Payments</span>
</a>
{% endif %}
{% if can_manage_users() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Users
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('auth.list_users') }}">
List Users
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">Dashboard</a>
</li>
{% if can_view_data() %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('search.search_page') }}">
<i class="fas fa-search"></i> Search Payments
</a>
<a class="navbar-item" href="{{ url_for('auth.add_user') }}">
Add User
</li>
{% endif %}
{% if can_manage_users() %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
Users
</a>
</div>
</div>
{% endif %}
{% if can_manage_batch_payments() %}
<a class="navbar-item" href="{{ url_for('main.batch_list') }}">
<span class="icon">
<i class="fas fa-file-invoice-dollar"></i>
</span>
<span>Payment Batches</span>
</a>
{% endif %}
{% if can_process_single_payments() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
<span class="icon">
<i class="fas fa-credit-card"></i>
</span>
<span>Single Payments</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('main.single_payments_list') }}">
<span class="icon">
<i class="fas fa-list"></i>
</span>
<span>View Payments</span>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('auth.list_users') }}">List Users</a></li>
<li><a class="dropdown-item" href="{{ url_for('auth.add_user') }}">Add User</a></li>
</ul>
</li>
{% endif %}
{% if can_manage_batch_payments() %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.batch_list') }}">
<i class="fas fa-file-invoice-dollar"></i> Payment Batches
</a>
<a class="navbar-item" href="{{ url_for('main.single_payment') }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>New Payment</span>
</li>
{% endif %}
{% if can_process_single_payments() %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-credit-card"></i> Single Payments
</a>
<hr class="navbar-divider">
<a class="navbar-item" href="{{ url_for('main.add_payment_method') }}">
<span class="icon">
<i class="fas fa-credit-card"></i>
</span>
<span>Add Payment Method</span>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('main.single_payments_list') }}"><i class="fas fa-list"></i> View Payments</a></li>
<li><a class="dropdown-item" href="{{ url_for('main.single_payment') }}"><i class="fas fa-plus"></i> New Payment</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('main.add_payment_method') }}"><i class="fas fa-credit-card"></i> Add Payment Method</a></li>
</ul>
</li>
{% endif %}
{% if can_manage_payment_plans() %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-calendar-alt"></i> Payment Plans
</a>
</div>
</div>
{% endif %}
{% if can_manage_payment_plans() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
<span class="icon">
<i class="fas fa-calendar-alt"></i>
</span>
<span>Payment Plans</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('main.payment_plans_list') }}">
<span class="icon">
<i class="fas fa-list"></i>
</span>
<span>View Plans</span>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('main.payment_plans_list') }}"><i class="fas fa-list"></i> View Plans</a></li>
<li><a class="dropdown-item" href="{{ url_for('main.payment_plans_create') }}"><i class="fas fa-plus"></i> New Plan</a></li>
</ul>
</li>
{% endif %}
{% if can_view_logs() %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.logs_list') }}">
<i class="fas fa-file-alt"></i> System Logs
</a>
<a class="navbar-item" href="{{ url_for('main.payment_plans_create') }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>New Plan</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('analytics.dashboard') }}">
<i class="fas fa-chart-line"></i> Analytics
</a>
</div>
</div>
{% endif %}
{% if can_view_logs() %}
<a class="navbar-item" href="{{ url_for('main.logs_list') }}">
<span class="icon">
<i class="fas fa-file-alt"></i>
</span>
<span>System Logs</span>
</a>
{% endif %}
{% if can_view_logs() %}
<a class="navbar-item" href="{{ url_for('analytics.dashboard') }}">
<span class="icon">
<i class="fas fa-chart-line"></i>
</span>
<span>Analytics</span>
</a>
{% endif %}
{% endif %}
</div>
</li>
{% endif %}
{% endif %}
</ul>
<div class="navbar-end">
{% if current_user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
{{ current_user.FullName }}&nbsp;&nbsp;
{% set user_permission = current_user.Permissions or 'None' %}
<span class="tag is-small is-{{ 'danger' if user_permission == 'Admin' else 'warning' if user_permission == 'Finance' else 'info' if user_permission == 'Helpdesk' else 'light' }}">
{{ user_permission }}
</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('auth.logout') }}">
Logout
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
{{ current_user.FullName }}
{% set user_permission = current_user.Permissions or 'None' %}
<span class="badge bg-{{ 'danger' if user_permission == 'Admin' else 'warning' if user_permission == 'Finance' else 'info' if user_permission == 'Helpdesk' else 'secondary' }}">
{{ user_permission }}
</span>
</a>
</div>
</div>
{% else %}
<div class="navbar-item">
<a class="button is-primary" href="{{ url_for('auth.login') }}">
Login
</a>
</div>
{% endif %}
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="btn btn-primary" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="section">
<main class="py-4">
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="notification is-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }}">
<button class="delete"></button>
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
@ -172,24 +130,14 @@
</div>
</main>
<footer class="footer">
<div class="content has-text-centered">
<p>
<footer class="footer mt-5 py-3 bg-dark text-center">
<div class="container">
<p class="text-light mb-0">
<strong style="color: var(--plutus-gold);">Plutus</strong> - Payment Processing System
</p>
</div>
</footer>
<script>
// Close notifications
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

195
templates/base_original.html

@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Plutus{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar is-dark" role="navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{{ url_for('main.index') }}">
<strong>Plutus</strong>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
{% if current_user.is_authenticated %}
<a class="navbar-item" href="{{ url_for('main.index') }}">
Dashboard
</a>
{% if can_view_data() %}
<a class="navbar-item" href="{{ url_for('search.search_page') }}">
<span class="icon">
<i class="fas fa-search"></i>
</span>
<span>Search Payments</span>
</a>
{% endif %}
{% if can_manage_users() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Users
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('auth.list_users') }}">
List Users
</a>
<a class="navbar-item" href="{{ url_for('auth.add_user') }}">
Add User
</a>
</div>
</div>
{% endif %}
{% if can_manage_batch_payments() %}
<a class="navbar-item" href="{{ url_for('main.batch_list') }}">
<span class="icon">
<i class="fas fa-file-invoice-dollar"></i>
</span>
<span>Payment Batches</span>
</a>
{% endif %}
{% if can_process_single_payments() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
<span class="icon">
<i class="fas fa-credit-card"></i>
</span>
<span>Single Payments</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('main.single_payments_list') }}">
<span class="icon">
<i class="fas fa-list"></i>
</span>
<span>View Payments</span>
</a>
<a class="navbar-item" href="{{ url_for('main.single_payment') }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>New Payment</span>
</a>
<hr class="navbar-divider">
<a class="navbar-item" href="{{ url_for('main.add_payment_method') }}">
<span class="icon">
<i class="fas fa-credit-card"></i>
</span>
<span>Add Payment Method</span>
</a>
</div>
</div>
{% endif %}
{% if can_manage_payment_plans() %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
<span class="icon">
<i class="fas fa-calendar-alt"></i>
</span>
<span>Payment Plans</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('main.payment_plans_list') }}">
<span class="icon">
<i class="fas fa-list"></i>
</span>
<span>View Plans</span>
</a>
<a class="navbar-item" href="{{ url_for('main.payment_plans_create') }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>New Plan</span>
</a>
</div>
</div>
{% endif %}
{% if can_view_logs() %}
<a class="navbar-item" href="{{ url_for('main.logs_list') }}">
<span class="icon">
<i class="fas fa-file-alt"></i>
</span>
<span>System Logs</span>
</a>
{% endif %}
{% if can_view_logs() %}
<a class="navbar-item" href="{{ url_for('analytics.dashboard') }}">
<span class="icon">
<i class="fas fa-chart-line"></i>
</span>
<span>Analytics</span>
</a>
{% endif %}
{% endif %}
</div>
<div class="navbar-end">
{% if current_user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
{{ current_user.FullName }}&nbsp;&nbsp;
{% set user_permission = current_user.Permissions or 'None' %}
<span class="tag is-small is-{{ 'danger' if user_permission == 'Admin' else 'warning' if user_permission == 'Finance' else 'info' if user_permission == 'Helpdesk' else 'light' }}">
{{ user_permission }}
</span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('auth.logout') }}">
Logout
</a>
</div>
</div>
{% else %}
<div class="navbar-item">
<a class="button is-primary" href="{{ url_for('auth.login') }}">
Login
</a>
</div>
{% endif %}
</div>
</div>
</nav>
<main class="section">
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="notification is-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }}">
<button class="delete"></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong style="color: var(--plutus-gold);">Plutus</strong> - Payment Processing System
</p>
</div>
</footer>
<script>
// Close notifications
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});
</script>
</body>
</html>

627
templates/hades/base.html

@ -0,0 +1,627 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hades - {% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--hades-bg-primary: linear-gradient(135deg, #1f2937 0%, #374151 50%, #4b5563 100%);
--hades-bg-overlay: rgba(255, 255, 255, 0.98);
--hades-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--hades-shadow-dark: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -2px rgb(0 0 0 / 0.2);
--hades-navbar-bg: rgba(31, 41, 55, 0.95);
--hades-text-light: #f9fafb;
--hades-text-muted: #d1d5db;
}
.Text-Area {
white-space: pre-wrap;
}
.Border-Right-Dotted {
border-right-style: dotted;
border-right: thick black;
}
html, body {
background: var(--hades-bg-primary);
background-attachment: fixed;
height: 1%;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-weight: 400;
color: var(--hades-text-light);
}
/* Add Hades background image - positioned behind everything */
body::after {
content: '';
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80vh;
background-image: url('/static/hades.png');
background-repeat: no-repeat;
background-position: left bottom;
background-size: auto 80vh;
opacity: 0.5;
z-index: -1;
pointer-events: none;
}
/* Main content area styling */
main {
min-height: calc(100vh - 76px);
}
/* Enhanced navbar styling for dark theme */
.navbar {
backdrop-filter: blur(10px);
background: var(--hades-navbar-bg) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: var(--hades-shadow-dark);
}
.navbar-brand {
font-weight: 600;
font-size: 1.5rem;
color: #dc2626 !important;
}
.navbar-nav .nav-link {
color: var(--hades-text-light) !important;
font-weight: 500;
transition: color 0.2s ease;
}
.navbar-nav .nav-link:hover {
color: #dc2626 !important;
}
.navbar-toggler {
border-color: rgba(255, 255, 255, 0.2);
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.8%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='m4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
/* Dropdown menu dark theme */
.dropdown-menu {
background-color: #374151;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: var(--hades-shadow-dark);
}
.dropdown-item {
color: var(--hades-text-light);
transition: all 0.2s ease;
}
.dropdown-item:hover,
.dropdown-item:focus {
background-color: #4b5563;
color: var(--hades-text-light);
}
.dropdown-divider {
border-color: rgba(255, 255, 255, 0.1);
}
/* Card improvements for dark theme */
.card {
backdrop-filter: blur(10px);
background: var(--hades-bg-overlay);
border: 1px solid rgba(0, 0, 0, 0.08);
box-shadow: var(--hades-shadow-dark);
color: #1f2937;
}
.card-header {
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
/* Table improvements */
.table {
background: var(--hades-bg-overlay);
color: #1f2937;
}
.table-light {
background: rgba(248, 249, 250, 0.98) !important;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.03);
}
/* Badge improvements */
.badge {
font-weight: 500;
letter-spacing: 0.025em;
}
/* Button improvements */
.btn {
font-weight: 500;
border-radius: 0.5rem;
transition: all 0.2s ease-in-out;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--hades-shadow);
}
/* Search form improvements for dark theme */
.navbar .form-control {
border-radius: 0.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.9);
color: #1f2937;
}
.navbar .form-control::placeholder {
color: #6b7280;
}
.navbar .form-control:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
background: rgba(255, 255, 255, 0.95);
}
.navbar .btn-outline-primary {
border-color: #3b82f6;
color: #3b82f6;
}
.navbar .btn-outline-primary:hover {
background-color: #3b82f6;
border-color: #3b82f6;
color: white;
}
/* Alert styling for dark theme */
.alert {
border-radius: 0.5rem;
box-shadow: var(--hades-shadow);
}
/* Modal improvements for dark theme */
.modal-content {
box-shadow: var(--hades-shadow-dark);
}
/* Responsive typography */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
letter-spacing: -0.025em;
}
/* Loading animation for better UX */
.loading {
opacity: 0.7;
pointer-events: none;
}
/* Better focus states for accessibility - FIXED */
.btn:focus,
.btn:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.btn-primary:focus,
.btn-primary:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.7);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.btn-outline-primary:focus,
.btn-outline-primary:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.7);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.btn-success:focus,
.btn-success:focus-visible,
.btn-outline-success:focus,
.btn-outline-success:focus-visible {
outline: 2px solid rgba(34, 197, 94, 0.7);
box-shadow: 0 0 0 0.2rem rgba(34, 197, 94, 0.25);
}
.btn-warning:focus,
.btn-warning:focus-visible,
.btn-outline-warning:focus,
.btn-outline-warning:focus-visible {
outline: 2px solid rgba(245, 158, 11, 0.7);
box-shadow: 0 0 0 0.2rem rgba(245, 158, 11, 0.25);
}
.btn-danger:focus,
.btn-danger:focus-visible,
.btn-outline-danger:focus,
.btn-outline-danger:focus-visible {
outline: 2px solid rgba(239, 68, 68, 0.7);
box-shadow: 0 0 0 0.2rem rgba(239, 68, 68, 0.25);
}
.btn-secondary:focus,
.btn-secondary:focus-visible,
.btn-outline-secondary:focus,
.btn-outline-secondary:focus-visible {
outline: 2px solid rgba(107, 114, 128, 0.7);
box-shadow: 0 0 0 0.2rem rgba(107, 114, 128, 0.25);
}
.btn-info:focus,
.btn-info:focus-visible,
.btn-outline-info:focus,
.btn-outline-info:focus-visible {
outline: 2px solid rgba(6, 182, 212, 0.7);
box-shadow: 0 0 0 0.2rem rgba(6, 182, 212, 0.25);
}
.form-control:focus,
.nav-link:focus {
outline: 2px solid rgba(59, 130, 246, 0.7);
outline-offset: 2px;
}
/* Custom scrollbar for webkit browsers - dark theme */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Toast container styling for dark theme */
.toast {
background-color: #374151;
color: var(--hades-text-light);
}
.toast-header {
background-color: #4b5563;
color: var(--hades-text-light);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Ensure text in cards remains dark and readable */
.card .text-muted {
color: #6b7280 !important;
}
.card .text-dark {
color: #1f2937 !important;
}
/* Input group button styling */
.input-group .btn-outline-primary {
border-color: #3b82f6;
color: #3b82f6;
}
.input-group .btn-outline-primary:hover {
background-color: #3b82f6;
border-color: #3b82f6;
color: white;
}
/* Responsive adjustments for Hades background */
@media (max-width: 768px) {
body::after {
background-size: 50vh auto;
opacity: 0.3;
}
}
@media (max-width: 576px) {
body::after {
background-size: 40vh auto;
opacity: 0.2;
}
}
.tooltip-custom {
cursor: help;
border-bottom: 1px dotted #6c757d;
position: relative;
}
/* JavaScript-powered tooltip positioning */
.tooltip-js {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 10000;
max-width: 250px;
text-align: center;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.tooltip-js.show {
opacity: 1;
}
.tooltip-js::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: rgba(0, 0, 0, 0.9);
}
/* Ensure cards don't interfere with tooltips */
.card {
position: relative;
z-index: 1;
}
.card:hover {
z-index: 2;
}
{% block styles %}
{% endblock %}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.index') }}">
<i class="bi bi-router me-2"></i>Hades
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarScroll" aria-controls="navbarScroll" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarScroll">
<ul class="navbar-nav me-auto my-2 my-lg-0">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('main.index') }}">
<i class="bi bi-house me-1"></i>Home
</a>
</li>
</ul>
<!-- Search Form with Tooltip -->
<form class="d-flex me-3" role="search" method="POST" action="/search/results">
<div class="input-group">
<input class="form-control tooltip-custom"
type="search"
placeholder="Search..."
aria-label="Search"
name="to_search"
data-tooltip="Can search for INTs, IVCs, ONT Serials, Beacon Serials/MACs">
<button class="btn btn-outline-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</form>
<!-- Profile Dropdown -->
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>Profile
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{{ url_for('auth.changepassword') }}">
<i class="bi bi-key me-2"></i>Change Password
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-2"></i>Logout
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<main>
{% block content %}
{% endblock %}
</main>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1200;">
<!-- Toasts will be dynamically added here -->
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script>
// Enhanced UX improvements
document.addEventListener('DOMContentLoaded', function() {
// Add loading states to forms
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function() {
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.innerHTML = '<i class="bi bi-arrow-clockwise spin me-1"></i>Loading...';
submitBtn.disabled = true;
}
});
});
// Auto-hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert-dismissible');
alerts.forEach(alert => {
setTimeout(() => {
const closeBtn = alert.querySelector('.btn-close');
if (closeBtn) closeBtn.click();
}, 5000);
});
// Add spinning animation for loading states
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin { animation: spin 1s linear infinite; }
`;
document.head.appendChild(style);
});
</script>
<!-- Enhanced tooltip JavaScript with search-specific functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Enhanced tooltip positioning
const tooltipElements = document.querySelectorAll('.tooltip-custom');
let activeTooltip = null;
tooltipElements.forEach(element => {
element.addEventListener('mouseenter', function(e) {
// Remove any existing tooltip
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'tooltip-js';
tooltip.textContent = this.getAttribute('data-tooltip');
document.body.appendChild(tooltip);
// Get positions including scroll offset
const rect = this.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
// Force tooltip to render to get dimensions
tooltip.style.visibility = 'hidden';
tooltip.style.display = 'block';
const tooltipRect = tooltip.getBoundingClientRect();
tooltip.style.visibility = '';
tooltip.style.display = '';
// Calculate position relative to document
let left = rect.left + scrollLeft + (rect.width / 2) - (tooltipRect.width / 2);
let top = rect.top + scrollTop - tooltipRect.height - 8;
// Adjust if tooltip goes off-screen horizontally
if (left < 10) {
left = 10;
}
if (left + tooltipRect.width > window.innerWidth - 10) {
left = window.innerWidth - tooltipRect.width - 10;
}
// Adjust if tooltip goes off-screen vertically (show below instead)
if (top < scrollTop + 10) {
top = rect.bottom + scrollTop + 8;
// Update arrow direction for bottom positioning
tooltip.innerHTML = this.getAttribute('data-tooltip');
tooltip.style.setProperty('--arrow-direction', 'up');
// Add upward arrow
const arrow = document.createElement('div');
arrow.style.cssText = `
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-bottom-color: rgba(0, 0, 0, 0.9);
`;
tooltip.appendChild(arrow);
}
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
// Show tooltip with smooth transition
setTimeout(() => tooltip.classList.add('show'), 10);
activeTooltip = tooltip;
});
element.addEventListener('mouseleave', function() {
if (activeTooltip) {
activeTooltip.classList.remove('show');
setTimeout(() => {
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
}, 200); // Match the CSS transition duration
}
});
// Also hide tooltip when input gets focus (user starts typing)
if (element.tagName.toLowerCase() === 'input') {
element.addEventListener('focus', function() {
if (activeTooltip) {
activeTooltip.classList.remove('show');
setTimeout(() => {
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
}, 200);
}
});
}
});
// Clean up tooltips when scrolling or clicking
window.addEventListener('scroll', function() {
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
});
document.addEventListener('click', function() {
if (activeTooltip) {
activeTooltip.remove();
activeTooltip = null;
}
});
});
</script>
{% block scripts %}
{% endblock %}
</body>
</html>

2161
templates/hades/location_ont_details.html

File diff suppressed because it is too large

61
templates/main/add_payment_method.html

@ -2,6 +2,67 @@
{% block title %}Add Payment Method - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card, .box {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert, .notification {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>

1219
templates/main/batch_detail.html

File diff suppressed because it is too large

121
templates/main/batch_list.html

@ -2,16 +2,92 @@
{% block title %}Payment Batches - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling for Payment Batches page */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
/* Ensure content is visible on top of background */
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
/* Page title styling */
.page-title {
background: linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(255, 191, 0, 0.95) 100%);
color: #2c2c2c;
padding: 1.5rem 2rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.4);
margin-bottom: 1.5rem;
}
/* Table container with enhanced visibility */
.table-responsive {
background-color: rgba(250, 248, 240, 0.98);
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
border: 2px solid rgba(212, 175, 55, 0.5);
}
/* Alert styling for better visibility */
.alert {
background-color: rgba(250, 248, 240, 0.98);
border: 2px solid rgba(212, 175, 55, 0.5);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<div class="level">
<div class="level-left">
<h1 class="title">Payment Batches</h1>
</div>
<div class="page-title text-center">
<h1 class="h2 mb-0">Payment Batches</h1>
</div>
{% if batches %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Batch ID</th>
@ -32,7 +108,7 @@
</td>
<td>{{ batch.Created.strftime('%Y-%m-%d %H:%M') if batch.Created else '-' }}</td>
<td>
<span class="tag is-info">{{ batch.payment_count or 0 }}</span>
<span class="badge bg-info">{{ batch.payment_count or 0 }}</span>
</td>
<td>
<strong>{{ batch.total_amount | currency }}</strong>
@ -44,41 +120,38 @@
{% if batch.payment_count and batch.payment_count > 0 %}
{% set success_rate = (batch.successful_count or 0) / batch.payment_count * 100 %}
{% if success_rate >= 90 %}
<span class="tag is-success">{{ "%.1f"|format(success_rate) }}%</span>
<span class="badge bg-success">{{ "%.1f"|format(success_rate) }}%</span>
{% elif success_rate >= 70 %}
<span class="tag is-warning">{{ "%.1f"|format(success_rate) }}%</span>
<span class="badge bg-warning">{{ "%.1f"|format(success_rate) }}%</span>
{% else %}
<span class="tag is-danger">{{ "%.1f"|format(success_rate) }}%</span>
<span class="badge bg-danger">{{ "%.1f"|format(success_rate) }}%</span>
{% endif %}
{% else %}
<span class="tag">0%</span>
<span class="badge bg-secondary">0%</span>
{% endif %}
</td>
<td>
<div class="tags">
<div>
{% if batch.successful_count %}
<span class="tag is-success is-small">{{ batch.successful_count }} Success</span>
<span class="badge bg-success me-1">{{ batch.successful_count }} Success</span>
{% endif %}
{% if batch.pending_count %}
<span class="tag is-warning is-small">{{ batch.pending_count }} Pending</span>
<span class="badge bg-warning me-1">{{ batch.pending_count }} Pending</span>
{% endif %}
{% if batch.failed_count %}
<span class="tag is-danger is-small">{{ batch.failed_count }} Failed</span>
<span class="badge bg-danger me-1">{{ batch.failed_count }} Failed</span>
{% endif %}
{% if batch.error_count %}
<span class="tag is-warning is-small">{{ batch.error_count }} Errors</span>
<span class="badge bg-warning me-1">{{ batch.error_count }} Errors</span>
{% endif %}
{% if not batch.successful_count and not batch.failed_count %}
<span class="tag is-light is-small">No Payments</span>
<span class="badge bg-light text-dark">No Payments</span>
{% endif %}
</div>
</td>
<td>
<a class="button is-primary is-small" href="{{ url_for('main.batch_detail', batch_id=batch.id) }}">
<span class="icon">
<i class="fas fa-eye"></i>
</span>
<span>View Details</span>
<a class="btn btn-primary btn-sm" href="{{ url_for('main.batch_detail', batch_id=batch.id) }}">
<i class="fas fa-eye"></i> View Details
</a>
</td>
</tr>
@ -87,8 +160,8 @@
</table>
</div>
{% else %}
<div class="notification is-info">
<p>No payment batches found. <a href="{{ url_for('main.index') }}">Return to dashboard</a>.</p>
<div class="alert alert-info">
<p class="mb-0">No payment batches found. <a href="{{ url_for('main.index') }}" class="alert-link">Return to dashboard</a>.</p>
</div>
{% endif %}
{% endblock %}

522
templates/main/batch_payment_detail.html

@ -0,0 +1,522 @@
{% extends "base.html" %}
{% block title %}Batch Payment #{{ payment.id }} - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
/* Ensure content is visible */
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
/* Enhanced visibility */
.card, .info-card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
/* Payment status banner colors with gradients */
.status-banner {
border-left: 5px solid;
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.status-banner.refund {
border-color: #9370db;
background: linear-gradient(135deg, #f8f4ff 0%, #ede7f6 100%);
}
.status-banner.refund-processing {
border-color: #ff8c00;
background: linear-gradient(135deg, #fff8f0 0%, #ffe5cc 100%);
}
.status-banner.success {
border-color: #28a745;
background: linear-gradient(135deg, #f0fff4 0%, #e8f5e9 100%);
}
.status-banner.failed {
border-color: #dc3545;
background: linear-gradient(135deg, #fff5f5 0%, #ffebee 100%);
}
.status-banner.pending {
border-color: #ffc107;
background: linear-gradient(135deg, #fffbf0 0%, #fff3cd 100%);
}
.status-banner .status-icon {
font-size: 2.5rem;
}
.status-banner .status-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.status-banner .status-text {
color: #6c757d;
margin-bottom: 0;
}
/* Card styling with subtle shadows */
.info-card {
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: box-shadow 0.3s ease;
}
.info-card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.card-header-custom {
background: linear-gradient(135deg, #d4af37 0%, #c5a028 100%);
color: #000;
font-weight: 600;
padding: 1rem 1.25rem;
border-radius: 0.5rem 0.5rem 0 0;
}
.card-header-error {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: #fff;
}
/* Table styling enhancements */
.table-config td:first-child {
background-color: #f8f9fa;
font-weight: 600;
width: 40%;
}
/* JSON code block styling */
pre code {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 0.375rem;
display: block;
overflow-x: auto;
max-height: 400px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
}
</style>
<script>
// Copy to clipboard functionality
function copyFormattedJSON(elementId) {
const element = document.getElementById(elementId);
const text = element.textContent || element.innerText;
navigator.clipboard.writeText(text).then(function() {
// Show temporary success message
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-check me-2"></i>Copied!';
button.classList.add('btn-success');
button.classList.remove('btn-info', 'btn-primary', 'btn-outline-secondary');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('btn-success');
// Restore original button class based on elementId
if (elementId.includes('pi-json')) {
button.classList.add('btn-info');
} else if (elementId.includes('followup')) {
button.classList.add('btn-primary');
} else if (elementId.includes('refund')) {
button.classList.add('btn-outline-secondary');
} else if (elementId.includes('error')) {
button.classList.add('btn-info');
}
}, 2000);
}).catch(function(err) {
console.error('Failed to copy text: ', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-check me-2"></i>Copied!';
button.classList.add('btn-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('btn-success');
}, 2000);
} catch (fallbackErr) {
console.error('Fallback copy failed: ', fallbackErr);
}
document.body.removeChild(textArea);
});
}
</script>
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('main.batch_list') }}">Payment Batches</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('main.batch_detail', batch_id=payment.PaymentBatch_ID) }}">Batch #{{ payment.PaymentBatch_ID }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Payment #{{ payment.id }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="h2 mb-1">Batch Payment #{{ payment.id }}</h1>
<p class="text-muted">Processed: {{ payment.Created.strftime('%Y-%m-%d %H:%M:%S') if payment.Created else 'Unknown' }}</p>
</div>
<a class="btn btn-light" href="{{ url_for('main.batch_detail', batch_id=payment.PaymentBatch_ID) }}">
<i class="fas fa-arrow-left me-2"></i>Back to Batch #{{ payment.PaymentBatch_ID }}
</a>
</div>
<!-- Payment Status Banner -->
{% if payment.Refund == True %}
<div class="status-banner refund">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="fas fa-undo status-icon me-4" style="color: #9370db;"></i>
<div>
<h2 class="status-title" style="color: #9370db;">Payment Refunded</h2>
<p class="status-text">This payment has been refunded to the customer.</p>
{% if payment.Stripe_Refund_Created %}
<p class="status-text small">Refunded: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</p>
{% endif %}
</div>
</div>
{% if payment.Refund_FollowUp != True %}
<span class="badge" style="background-color: #9370db; font-size: 1rem; padding: 0.5rem 1rem;">Completed</span>
{% endif %}
</div>
</div>
{% elif payment.Refund_FollowUp == True %}
<div class="status-banner refund-processing">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="fas fa-clock status-icon me-4" style="color: #ff8c00;"></i>
<div>
<h2 class="status-title" style="color: #ff8c00;">Refund Processing</h2>
<p class="status-text">A refund is being processed for this payment.</p>
<p class="status-text small">BECS Direct Debit refunds can take several business days to complete.</p>
{% if payment.Stripe_Refund_Created %}
<p class="status-text small">Initiated: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% elif payment.PI_FollowUp == True %}
<div class="status-banner pending">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="fas fa-clock status-icon text-warning me-4"></i>
<div>
<h2 class="status-title text-warning">Payment Pending</h2>
<p class="status-text">This payment is still being processed by the bank.</p>
<p class="status-text small">BECS Direct Debit payments can take several business days to complete.</p>
{% if payment.PI_Last_Check %}
<p class="status-text small">Last checked: {{ payment.PI_Last_Check.strftime('%Y-%m-%d %H:%M:%S') }}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% elif payment.Success == True %}
<div class="status-banner success">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<i class="fas fa-check-circle status-icon text-success me-4"></i>
<div>
<h2 class="status-title text-success">Payment Successful</h2>
<p class="status-text">This payment has been completed successfully.</p>
</div>
</div>
</div>
</div>
{% elif payment.Success == False %}
<div class="status-banner failed">
<div class="d-flex align-items-center">
<i class="fas fa-times-circle status-icon text-danger me-4"></i>
<div>
<h2 class="status-title text-danger">Payment Failed</h2>
<p class="status-text">This payment could not be completed.</p>
</div>
</div>
</div>
{% else %}
<div class="status-banner pending">
<div class="d-flex align-items-center">
<i class="fas fa-clock status-icon text-warning me-4"></i>
<div>
<h2 class="status-title text-warning">Payment Pending</h2>
<p class="status-text">This payment is still being processed.</p>
</div>
</div>
</div>
{% endif %}
<!-- Payment Details -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card info-card">
<div class="card-header-custom">
<i class="fas fa-info-circle me-2"></i>Payment Information
</div>
<div class="card-body">
<table class="table table-sm table-config mb-0">
<tbody>
<tr>
<td>Payment ID</td>
<td><strong>#{{ payment.id }}</strong></td>
</tr>
<tr>
<td>Batch ID</td>
<td>
<a href="{{ url_for('main.batch_detail', batch_id=payment.PaymentBatch_ID) }}"
class="fw-semibold">#{{ payment.PaymentBatch_ID }}</a>
</td>
</tr>
<tr>
<td>Splynx Customer ID</td>
<td>
{% if payment.Splynx_ID %}
<a href="https://billing.interphone.com.au/admin/customers/view?id={{ payment.Splynx_ID }}"
target="_blank" class="fw-semibold">{{ payment.Splynx_ID }}</a>
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Stripe Customer ID</td>
<td><code class="small">{{ payment.Stripe_Customer_ID or '-' }}</code></td>
</tr>
<tr>
<td>Payment Intent</td>
<td><code class="small">{{ payment.Payment_Intent or '-' }}</code></td>
</tr>
<tr>
<td>Payment Method</td>
<td>
{% if payment.Payment_Method %}
<span class="badge bg-info">{{ payment.Payment_Method }}</span>
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ payment.Created.strftime('%Y-%m-%d %H:%M:%S') if payment.Created else '-' }}</td>
</tr>
<tr>
<td>Processed By</td>
<td>{{ payment.processed_by or 'Unknown' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card info-card">
<div class="card-header-custom">
<i class="fas fa-dollar-sign me-2"></i>Financial Details
</div>
<div class="card-body">
<table class="table table-sm table-config mb-3">
<tbody>
<tr>
<td>Payment Amount</td>
<td><strong class="text-success">${{ "%.2f"|format(payment.Payment_Amount|abs) if payment.Payment_Amount else '0.00' }} AUD</strong></td>
</tr>
<tr>
<td>Stripe Fee</td>
<td>${{ "%.2f"|format(payment.Fee_Stripe|abs) if payment.Fee_Stripe else '0.00' }}</td>
</tr>
<tr>
<td>Tax Fee</td>
<td>${{ "%.2f"|format(payment.Fee_Tax|abs) if payment.Fee_Tax else '0.00' }}</td>
</tr>
<tr>
<td>Total Fees</td>
<td>${{ "%.2f"|format(payment.Fee_Total|abs) if payment.Fee_Total else '0.00' }}</td>
</tr>
</tbody>
</table>
{% if payment.PI_FollowUp %}
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Follow-up Required:</strong> This payment requires additional processing.
{% if payment.PI_Last_Check %}
<br><small>Last checked: {{ payment.PI_Last_Check.strftime('%Y-%m-%d %H:%M:%S') }}</small>
{% endif %}
</div>
{% endif %}
{% if payment.Refund == True %}
<div class="alert" style="background-color: #f8f4ff; border-color: #9370db; color: #5a4570;">
<i class="fas fa-undo me-2" style="color: #9370db;"></i>
<strong style="color: #9370db;">Refund Completed:</strong> This payment has been successfully refunded.
{% if payment.Stripe_Refund_Created %}
<br><small>Refunded: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</small>
{% endif %}
{% if payment.Stripe_Refund_ID %}
<br><small>Refund ID: <code class="small">{{ payment.Stripe_Refund_ID }}</code></small>
{% endif %}
</div>
{% elif payment.Refund_FollowUp == True %}
<div class="alert alert-warning">
<i class="fas fa-clock me-2" style="color: #ff8c00;"></i>
<strong style="color: #ff8c00;">Refund Processing:</strong> A refund for this payment is currently being processed by the bank.
{% if payment.Stripe_Refund_Created %}
<br><small>Initiated: {{ payment.Stripe_Refund_Created.strftime('%Y-%m-%d %H:%M:%S') }}</small>
{% endif %}
{% if payment.Stripe_Refund_ID %}
<br><small>Refund ID: <code class="small">{{ payment.Stripe_Refund_ID }}</code></small>
{% endif %}
<br><small><em>BECS Direct Debit refunds typically take 3-5 business days to complete.</em></small>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Error Information -->
{% if payment.Error %}
<div class="card info-card mb-4">
<div class="card-header-error">
<i class="fas fa-exclamation-triangle me-2"></i>Payment Error Details
</div>
<div class="card-body">
<div class="alert alert-danger">
<h5 class="h6">Payment Error</h5>
<p>An error occurred during payment processing.</p>
<details class="mt-3">
<summary class="text-muted">Technical Details</summary>
<pre class="mt-2">{{ payment.Error }}</pre>
</details>
</div>
</div>
</div>
{% endif %}
<!-- JSON Data -->
<div class="row">
{% if payment.PI_JSON %}
<div class="col-md-{% if payment.PI_FollowUp_JSON or payment.Refund_JSON %}6{% else %}12{% endif %} mb-4">
<div class="card info-card">
<div class="card-header-custom">
<i class="fas fa-code me-2"></i>Payment Intent JSON
</div>
<div class="card-body">
<button class="btn btn-sm btn-info mb-3" onclick="copyFormattedJSON('pi-json-content')">
<i class="fas fa-copy me-2"></i>Copy JSON
</button>
<pre><code>{{ payment.PI_JSON | format_json }}</code></pre>
<div id="pi-json-content" style="display: none;">{{ payment.PI_JSON | format_json }}</div>
</div>
</div>
</div>
{% endif %}
{% if payment.PI_FollowUp_JSON %}
<div class="col-md-6 mb-4">
<div class="card info-card">
<div class="card-header-custom">
<i class="fas fa-redo me-2"></i>Follow-up JSON
</div>
<div class="card-body">
<button class="btn btn-sm btn-primary mb-3" onclick="copyFormattedJSON('followup-json-content')">
<i class="fas fa-copy me-2"></i>Copy JSON
</button>
<pre><code>{{ payment.PI_FollowUp_JSON | format_json }}</code></pre>
<div id="followup-json-content" style="display: none;">{{ payment.PI_FollowUp_JSON | format_json }}</div>
</div>
</div>
</div>
{% endif %}
{% if payment.Refund_JSON %}
<div class="col-md-6 mb-4">
<div class="card info-card">
<div class="card-header-custom" style="background: linear-gradient(135deg, #9370db 0%, #7d5ba6 100%); color: #fff;">
<i class="fas fa-undo me-2"></i>Refund JSON
</div>
<div class="card-body">
<button class="btn btn-sm btn-outline-secondary mb-3" style="border-color: #9370db; color: #9370db;" onclick="copyFormattedJSON('refund-json-content')">
<i class="fas fa-copy me-2"></i>Copy JSON
</button>
<pre><code>{{ payment.Refund_JSON | format_json }}</code></pre>
<div id="refund-json-content" style="display: none;">{{ payment.Refund_JSON | format_json }}</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

24
templates/main/index.html

@ -15,19 +15,17 @@ body::before {
{% endblock %}
{% block content %}
<div class="hero is-primary">
<div class="hero-body has-text-centered">
<h1 class="title">
Welcome to Plutus
</h1>
<h2 class="subtitle">
Payment Processing System
</h2>
</div>
<div class="hero text-center mb-4">
<h1 class="display-4 fw-bold">
Welcome to Plutus
</h1>
<p class="lead">
Payment Processing System
</p>
</div>
<img src="{{ url_for('static', filename='images/plutus3.JPG') }}" alt="Plutus - God of Wealth" class="plutus-image">
<div class="notification is-info">
<h4 class="title is-5">Welcome, {{ current_user.FullName }}!</h4>
<p>You are successfully logged into the Plutus payment processing system.</p>
<img src="{{ url_for('static', filename='images/plutus3.JPG') }}" alt="Plutus - God of Wealth" class="plutus-image img-fluid rounded shadow-sm mb-4">
<div class="alert alert-info">
<h4 class="h5 mb-2">Welcome, {{ current_user.FullName }}!</h4>
<p class="mb-0">You are successfully logged into the Plutus payment processing system.</p>
</div>
{% endblock %}

477
templates/main/logs_list.html

@ -2,268 +2,304 @@
{% block title %}System Logs - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.table-responsive {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">System Logs</a></li>
</ul>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">System Logs</li>
</ol>
</nav>
<div class="level">
<div class="level-left">
<div>
<h1 class="title">System Logs</h1>
<p class="subtitle">User activity and system audit trail</p>
</div>
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="h2 mb-1">System Logs</h1>
<p class="text-muted">User activity and system audit trail</p>
</div>
<div class="level-right">
<div class="field is-grouped">
<div class="control">
<button class="button is-info" onclick="exportLogs()">
<span class="icon"><i class="fas fa-download"></i></span>
<span>Export Logs</span>
</button>
</div>
</div>
<div>
<button class="btn btn-info" onclick="exportLogs()">
<i class="fas fa-download"></i> Export Logs
</button>
</div>
</div>
<!-- Filter Controls -->
<div class="box">
<h2 class="title is-5">
<span class="icon"><i class="fas fa-filter"></i></span>
Filters
</h2>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<label class="label is-small">Search:</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input class="input" type="text" id="searchInput" placeholder="Search logs, actions, details...">
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
<div class="card shadow mb-4">
<div class="card-body">
<h2 class="h5 mb-3">
<i class="fas fa-filter"></i> Filters
</h2>
<div class="row g-3">
<div class="col-md-6 col-lg-4">
<label class="form-label form-label-sm">Search:</label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input class="form-control" type="text" id="searchInput" placeholder="Search logs, actions, details...">
</div>
</div>
</div>
<div class="control">
<label class="label is-small">User:</label>
<div class="select">
<select id="userFilter">
<div class="col-md-6 col-lg-2">
<label class="form-label form-label-sm">User:</label>
<select class="form-select form-select-sm" id="userFilter">
<option value="">All Users</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.FullName }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Action:</label>
<div class="select">
<select id="actionFilter">
<div class="col-md-6 col-lg-2">
<label class="form-label form-label-sm">Action:</label>
<select class="form-select form-select-sm" id="actionFilter">
<option value="">All Actions</option>
{% for action in actions %}
<option value="{{ action }}">{{ action }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Entity Type:</label>
<div class="select">
<select id="entityTypeFilter">
<div class="col-md-6 col-lg-2">
<label class="form-label form-label-sm">Entity Type:</label>
<select class="form-select form-select-sm" id="entityTypeFilter">
<option value="">All Types</option>
{% for entity_type in entity_types %}
<option value="{{ entity_type }}">{{ entity_type }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Date From:</label>
<input class="input" type="date" id="dateFromFilter">
</div>
<div class="control">
<label class="label is-small">Date To:</label>
<input class="input" type="date" id="dateToFilter">
</div>
<div class="col-md-6 col-lg-3">
<label class="form-label form-label-sm">Date From:</label>
<input class="form-control form-control-sm" type="date" id="dateFromFilter">
</div>
<div class="control">
<button class="button is-small is-info" onclick="applyFilters()">
<span class="icon"><i class="fas fa-search"></i></span>
<span>Apply Filters</span>
</button>
</div>
<div class="col-md-6 col-lg-3">
<label class="form-label form-label-sm">Date To:</label>
<input class="form-control form-control-sm" type="date" id="dateToFilter">
</div>
<div class="control">
<button class="button is-small is-light" onclick="clearFilters()">
<span class="icon"><i class="fas fa-times"></i></span>
<span>Clear</span>
</button>
<div class="col-md-6 col-lg-3 d-flex align-items-end">
<button class="btn btn-sm btn-info me-2" onclick="applyFilters()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-sm btn-light" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
<!-- Results Summary -->
<div class="notification is-info is-light" id="filterResults" style="display: none;">
<div class="alert alert-info" id="filterResults" style="display: none;">
<span id="resultCount">0</span> of {{ logs|length }} log entries shown
</div>
<!-- Logs Table -->
<div class="box">
<h2 class="title is-5">
<span class="icon"><i class="fas fa-list"></i></span>
Log Entries
</h2>
{% if logs %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable" id="logsTable">
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Action</th>
<th>Entity</th>
<th>Details</th>
<th>IP Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="logsTableBody">
{% for log in logs %}
<tr>
<td>
<span class="is-size-7">{{ log.Added.strftime('%Y-%m-%d') }}</span><br>
<span class="is-size-7 has-text-grey">{{ log.Added.strftime('%H:%M:%S') }}</span>
</td>
<td>
<div class="media">
<div class="media-content">
<div class="card shadow">
<div class="card-body">
<h2 class="h5 mb-3">
<i class="fas fa-list"></i> Log Entries
</h2>
{% if logs %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="logsTable">
<thead>
<tr>
<th>Timestamp</th>
<th>User</th>
<th>Action</th>
<th>Entity</th>
<th>Details</th>
<th>IP Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="logsTableBody">
{% for log in logs %}
<tr>
<td>
<small class="d-block">{{ log.Added.strftime('%Y-%m-%d') }}</small>
<small class="text-muted">{{ log.Added.strftime('%H:%M:%S') }}</small>
</td>
<td>
<div>
<strong>{{ log.user_name or 'System' }}</strong>
{% if log.User_ID %}
<br><small class="has-text-grey">ID: {{ log.User_ID }}</small>
<br><small class="text-muted">ID: {{ log.User_ID }}</small>
{% endif %}
</div>
</div>
</td>
<td>
{% if log.Action %}
<span class="tag is-info is-light">{{ log.Action }}</span>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.Entity_Type %}
<div>
<span class="tag is-primary is-light">{{ log.Entity_Type }}</span>
{% if log.Entity_ID %}
<br><small class="has-text-grey">ID: {{ log.Entity_ID }}</small>
</td>
<td>
{% if log.Action %}
<span class="badge bg-info">{{ log.Action }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.Log_Entry %}
<div class="content is-small">
{{ log.Log_Entry[:100] }}{% if log.Log_Entry|length > 100 %}...{% endif %}
</div>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.IP_Address %}
<code class="is-small">{{ log.IP_Address }}</code>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
<div class="buttons are-small">
<button class="button is-info is-outlined" onclick="showLogDetail({{ log.id }})">
<span class="icon"><i class="fas fa-eye"></i></span>
<span>View</span>
</td>
<td>
{% if log.Entity_Type %}
<div>
<span class="badge bg-primary">{{ log.Entity_Type }}</span>
{% if log.Entity_ID %}
<br><small class="text-muted">ID: {{ log.Entity_ID }}</small>
{% endif %}
</div>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if log.Log_Entry %}
<small>
{{ log.Log_Entry[:100] }}{% if log.Log_Entry|length > 100 %}...{% endif %}
</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
{% if log.IP_Address %}
<code class="small">{{ log.IP_Address }}</code>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<button class="btn btn-info btn-sm" onclick="showLogDetail({{ log.id }})">
<i class="fas fa-eye"></i> View
</button>
</div>
</td>
</tr>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination %}
<nav aria-label="Logs pagination">
<ul class="pagination justify-content-center mt-3">
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
<a class="page-link" href="{% if pagination.has_prev %}{{ url_for('main.logs_list', page=pagination.prev_num, **request.args) }}{% else %}#{% endif %}">Previous</a>
</li>
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('main.logs_list', page=page_num, **request.args) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if pagination.has_prev %}
<a class="pagination-previous" href="{{ url_for('main.logs_list', page=pagination.prev_num, **request.args) }}">Previous</a>
{% else %}
<a class="pagination-previous" disabled>Previous</a>
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
<a class="page-link" href="{% if pagination.has_next %}{{ url_for('main.logs_list', page=pagination.next_num, **request.args) }}{% else %}#{% endif %}">Next</a>
</li>
</ul>
</nav>
{% endif %}
{% if pagination.has_next %}
<a class="pagination-next" href="{{ url_for('main.logs_list', page=pagination.next_num, **request.args) }}">Next page</a>
{% else %}
<a class="pagination-next" disabled>Next page</a>
<div class="alert alert-info">
<p class="mb-0">No log entries found.</p>
</div>
{% endif %}
<ul class="pagination-list">
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li><a class="pagination-link" href="{{ url_for('main.logs_list', page=page_num, **request.args) }}">{{ page_num }}</a></li>
{% else %}
<li><a class="pagination-link is-current" href="#">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="notification is-info">
<p>No log entries found.</p>
</div>
{% endif %}
</div>
<!-- Log Detail Modal -->
<div class="modal" id="logDetailModal">
<div class="modal-background" onclick="hideModal('logDetailModal')"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">
<span class="icon"><i class="fas fa-file-alt"></i></span>
Log Entry Details
</p>
<button class="delete" aria-label="close" onclick="hideModal('logDetailModal')"></button>
</header>
<section class="modal-card-body">
<div id="logDetailContent">
<!-- Log details will be populated here -->
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-labelledby="logDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logDetailModalLabel">
<i class="fas fa-file-alt"></i> Log Entry Details
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="logDetailContent">
<!-- Log details will be populated here -->
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-info" onclick="copyLogDetails()">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Copy Details</span>
</button>
<button class="button" onclick="hideModal('logDetailModal')">Close</button>
</footer>
<div class="modal-footer">
<button class="btn btn-info" onclick="copyLogDetails()">
<i class="fas fa-copy"></i> Copy Details
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
@ -397,8 +433,8 @@ function showLogDetail(logId) {
if (data.success) {
const log = data.log;
const detailHtml = `
<div class="content">
<table class="table is-fullwidth">
<div>
<table class="table table-bordered">
<tbody>
<tr>
<td><strong>ID</strong></td>
@ -432,10 +468,12 @@ function showLogDetail(logId) {
</table>
${log.Log_Entry ? `
<div class="field">
<label class="label">Full Details:</label>
<div class="box">
<pre class="has-text-dark">${log.Log_Entry}</pre>
<div class="mb-3">
<label class="form-label"><strong>Full Details:</strong></label>
<div class="card">
<div class="card-body">
<pre class="mb-0">${log.Log_Entry}</pre>
</div>
</div>
</div>
` : ''}
@ -443,7 +481,8 @@ function showLogDetail(logId) {
`;
document.getElementById('logDetailContent').innerHTML = detailHtml;
document.getElementById('logDetailModal').classList.add('is-active');
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
modal.show();
} else {
alert('Failed to load log details: ' + data.error);
}
@ -454,22 +493,20 @@ function showLogDetail(logId) {
});
}
function hideModal(modalId) {
document.getElementById(modalId).classList.remove('is-active');
}
function copyLogDetails() {
const content = document.getElementById('logDetailContent').innerText;
navigator.clipboard.writeText(content).then(function() {
// Show temporary success message
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.innerHTML = '<span class="icon"><i class="fas fa-check"></i></span><span>Copied!</span>';
button.classList.add('is-success');
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
button.classList.remove('btn-info');
button.classList.add('btn-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('is-success');
button.classList.remove('btn-success');
button.classList.add('btn-info');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy text: ', err);
@ -498,13 +535,5 @@ function exportLogs() {
// Open export URL in new window
window.open(`/logs/export?${params.toString()}`, '_blank');
}
// Close modal on Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const activeModals = document.querySelectorAll('.modal.is-active');
activeModals.forEach(modal => modal.classList.remove('is-active'));
}
});
</script>
{% endblock %}

1139
templates/main/payment_detail.html

File diff suppressed because it is too large

61
templates/main/payment_plans_form.html

@ -2,6 +2,67 @@
{% block title %}{% if edit_mode %}Edit Payment Plan{% else %}Create Payment Plan{% endif %} - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card, .box {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert, .notification {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>

346
templates/main/payment_plans_list.html

@ -2,190 +2,242 @@
{% block title %}Payment Plans - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.table-responsive {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">Payment Plans</a></li>
</ul>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Payment Plans</li>
</ol>
</nav>
<div class="level">
<div class="level-left">
<div>
<h1 class="title">Payment Plans</h1>
<p class="subtitle">Recurring payment management</p>
</div>
</div>
<div class="level-right">
<a class="button is-primary" href="{{ url_for('main.payment_plans_create') }}">
<span class="icon"><i class="fas fa-plus"></i></span>
<span>New Payment Plan</span>
</a>
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="h2 mb-1">Payment Plans</h1>
<p class="text-muted">Recurring payment management</p>
</div>
<a class="btn btn-primary" href="{{ url_for('main.payment_plans_create') }}">
<i class="fas fa-plus"></i> New Payment Plan
</a>
</div>
<!-- Summary Statistics -->
<div class="columns">
<div class="column is-3">
<div class="box has-text-centered">
<p class="title is-4 has-text-success">{{ summary.active_plans }}</p>
<p class="subtitle is-6">Active Plans</p>
<div class="row mb-4">
<div class="col-md-3">
<div class="card shadow text-center">
<div class="card-body">
<p class="h4 text-success mb-1">{{ summary.active_plans }}</p>
<p class="text-muted mb-0">Active Plans</p>
</div>
</div>
</div>
<div class="column is-3">
<div class="box has-text-centered">
<p class="title is-4 has-text-warning">{{ summary.inactive_plans }}</p>
<p class="subtitle is-6">Inactive Plans</p>
<div class="col-md-3">
<div class="card shadow text-center">
<div class="card-body">
<p class="h4 text-warning mb-1">{{ summary.inactive_plans }}</p>
<p class="text-muted mb-0">Inactive Plans</p>
</div>
</div>
</div>
<div class="column is-3">
<div class="box has-text-centered">
<p class="title is-4 has-text-info">{{ summary.total_plans }}</p>
<p class="subtitle is-6">Total Plans</p>
<div class="col-md-3">
<div class="card shadow text-center">
<div class="card-body">
<p class="h4 text-info mb-1">{{ summary.total_plans }}</p>
<p class="text-muted mb-0">Total Plans</p>
</div>
</div>
</div>
<div class="column is-3">
<div class="box has-text-centered">
<p class="title is-4 has-text-primary">{{ summary.total_recurring_amount | currency }}</p>
<p class="subtitle is-6">Monthly Recurring</p>
<div class="col-md-3">
<div class="card shadow text-center">
<div class="card-body">
<p class="h4 text-primary mb-1">{{ summary.total_recurring_amount | currency }}</p>
<p class="text-muted mb-0">Monthly Recurring</p>
</div>
</div>
</div>
</div>
<!-- Payment Plans Table -->
<div class="box">
<div class="level">
<div class="level-left">
<h2 class="title is-4">Payment Plans</h2>
</div>
<div class="level-right">
<div class="field">
<p class="control has-icons-left">
<input class="input" type="text" id="searchInput" placeholder="Search Customer ID, Amount...">
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
</p>
<div class="card shadow">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h4 mb-0">Payment Plans</h2>
<div class="input-group" style="max-width: 350px;">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input class="form-control" type="text" id="searchInput" placeholder="Search Customer ID, Amount...">
</div>
</div>
</div>
<!-- Filter Controls -->
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<label class="label is-small">Filter by Status:</label>
<div class="select is-small">
<select id="statusFilter">
<!-- Filter Controls -->
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label form-label-sm">Filter by Status:</label>
<select class="form-select form-select-sm" id="statusFilter">
<option value="">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Filter by Frequency:</label>
<div class="select is-small">
<select id="frequencyFilter">
<div class="col-md-6">
<label class="form-label form-label-sm">Filter by Frequency:</label>
<select class="form-select form-select-sm" id="frequencyFilter">
<option value="">All</option>
<option value="Weekly">Weekly</option>
<option value="Fortnightly">Fortnightly</option>
</select>
</div>
</div>
</div>
{% if plans %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable" id="plansTable">
<thead>
<tr>
<th>Plan ID</th>
<th>Customer</th>
<th>Splynx ID</th>
<th>Amount</th>
<th>Frequency</th>
<th>Start Date</th>
<th>Status</th>
<th>Created</th>
<th>Created By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for plan in plans %}
<tr data-status="{{ 'active' if plan.Enabled else 'inactive' }}"
data-frequency="{{ plan.Frequency }}"
data-splynx-id="{{ plan.Splynx_ID }}"
data-amount="{{ plan.Amount }}"
data-customer-name="">
<td>
<a href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}" class="has-text-weight-semibold">
#{{ plan.id }}
</a>
</td>
<td>
<span class="customer-name" data-splynx-id="{{ plan.Splynx_ID }}">
<span class="icon"><i class="fas fa-spinner fa-spin"></i></span>
Loading...
</span>
</td>
<td>
<a href="https://billing.interphone.com.au/admin/customers/view?id={{ plan.Splynx_ID }}"
target="_blank" class="tag is-info">{{ plan.Splynx_ID }}</a>
</td>
<td>
<strong>{{ plan.Amount | currency }}</strong>
</td>
<td>
<span class="tag {% if plan.Frequency == 'Weekly' %}is-warning{% elif plan.Frequency == 'Fortnightly' %}is-info{% else %}is-light{% endif %}">
{{ plan.Frequency }}
</span>
</td>
<td>{{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }}</td>
<td>
{% if plan.Enabled %}
<span class="tag is-success">Active</span>
{% else %}
<span class="tag is-danger">Inactive</span>
{% endif %}
</td>
<td>{{ plan.Created.strftime('%Y-%m-%d %H:%M') if plan.Created else '-' }}</td>
<td>{{ plan.created_by or 'Unknown' }}</td>
<td>
<div class="field is-grouped">
<div class="control">
<a class="button is-small is-info"
{% if plans %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="plansTable">
<thead>
<tr>
<th>Plan ID</th>
<th>Customer</th>
<th>Splynx ID</th>
<th>Amount</th>
<th>Frequency</th>
<th>Start Date</th>
<th>Status</th>
<th>Created</th>
<th>Created By</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for plan in plans %}
<tr data-status="{{ 'active' if plan.Enabled else 'inactive' }}"
data-frequency="{{ plan.Frequency }}"
data-splynx-id="{{ plan.Splynx_ID }}"
data-amount="{{ plan.Amount }}"
data-customer-name="">
<td>
<a href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}" class="fw-semibold">
#{{ plan.id }}
</a>
</td>
<td>
<span class="customer-name" data-splynx-id="{{ plan.Splynx_ID }}">
<i class="fas fa-spinner fa-spin"></i>
Loading...
</span>
</td>
<td>
<a href="https://billing.interphone.com.au/admin/customers/view?id={{ plan.Splynx_ID }}"
target="_blank" class="badge bg-info">{{ plan.Splynx_ID }}</a>
</td>
<td>
<strong>{{ plan.Amount | currency }}</strong>
</td>
<td>
<span class="badge {% if plan.Frequency == 'Weekly' %}bg-warning{% elif plan.Frequency == 'Fortnightly' %}bg-info{% else %}bg-secondary{% endif %}">
{{ plan.Frequency }}
</span>
</td>
<td>{{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }}</td>
<td>
{% if plan.Enabled %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</td>
<td>{{ plan.Created.strftime('%Y-%m-%d %H:%M') if plan.Created else '-' }}</td>
<td>{{ plan.created_by or 'Unknown' }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a class="btn btn-info btn-sm"
href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}">
<span class="icon"><i class="fas fa-eye"></i></span>
<i class="fas fa-eye"></i>
</a>
</div>
<div class="control">
<a class="button is-small is-warning"
<a class="btn btn-warning btn-sm"
href="{{ url_for('main.payment_plans_edit', plan_id=plan.id) }}">
<span class="icon"><i class="fas fa-edit"></i></span>
<i class="fas fa-edit"></i>
</a>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="has-text-centered py-6">
<span class="icon is-large has-text-grey-light">
<i class="fas fa-calendar-alt fa-3x"></i>
</span>
<p class="title is-5 has-text-grey">No Payment Plans Found</p>
<p class="subtitle is-6 has-text-grey">Get started by creating your first payment plan.</p>
<a class="button is-primary" href="{{ url_for('main.payment_plans_create') }}">
<span class="icon"><i class="fas fa-plus"></i></span>
<span>Create Payment Plan</span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-calendar-alt fa-3x text-muted mb-3"></i>
<p class="h5 text-muted">No Payment Plans Found</p>
<p class="text-muted">Get started by creating your first payment plan.</p>
<a class="btn btn-primary" href="{{ url_for('main.payment_plans_create') }}">
<i class="fas fa-plus"></i> Create Payment Plan
</a>
</div>
{% endif %}
</div>
{% endif %}
</div>
<script>
@ -205,12 +257,12 @@ document.addEventListener('DOMContentLoaded', function() {
const row = element.closest('tr');
row.dataset.customerName = data.name.toLowerCase();
} else {
element.innerHTML = '<span class="has-text-danger">Unknown Customer</span>';
element.innerHTML = '<span class="text-danger">Unknown Customer</span>';
}
})
.catch(error => {
console.error('Error fetching customer:', error);
element.innerHTML = '<span class="has-text-danger">Error Loading</span>';
element.innerHTML = '<span class="text-danger">Error Loading</span>';
});
});
});

585
templates/main/single_payment.html

@ -2,29 +2,80 @@
{% block title %}Single Payment - Plutus{% endblock %}
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
/* (No page-level z-index; Bootstrap modals handle their own layering) */
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card, .box {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
</style>
{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">Single Payment</a></li>
</ul>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Single Payment</li>
</ol>
</nav>
<div class="level">
<div class="level-left">
<div>
<h1 class="title">Single Payment Processing</h1>
<p class="subtitle">Process individual customer payments through Stripe</p>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h2">Single Payment Processing</h1>
<p class="text-muted">Process individual customer payments through Stripe</p>
</div>
</div>
<!-- Single Payment Form -->
<div class="box">
<div class="card">
<div class="card-body">
<!-- Step 1: Enter Splynx ID -->
<div id="step1" class="payment-step">
<h2 class="title is-4">
<span class="icon"><i class="fas fa-search"></i></span>
<h2 class="h4">
<i class="fas fa-search me-2"></i>
Customer Lookup
</h2>
@ -37,14 +88,14 @@
</div>
<!-- Loading State -->
<div id="loading" class="has-text-centered py-5 is-hidden">
<div id="loading" class="text-center py-5 d-none">
<div class="spinner"></div>
<p class="mt-3">Fetching customer details...</p>
</div>
<!-- Error State -->
<div id="customerError" class="notification is-danger is-hidden">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
<div id="customerError" class="alert alert-danger d-none">
<i class="fas fa-exclamation-triangle me-2"></i>
<span id="errorMessage">Customer not found or error occurred</span>
</div>
@ -58,19 +109,86 @@
</div>
</div>
<!-- Step 2: Confirm Customer & Enter Amount -->
<div id="step2" class="payment-step is-hidden">
<h2 class="title is-4">
<span class="icon"><i class="fas fa-user-check"></i></span>
<!-- Step 2: Select Invoices (NEW) -->
<div id="step2" class="payment-step d-none">
<h2 class="h4">
<i class="fas fa-file-invoice me-2"></i>
Select Invoices to Pay
</h2>
<!-- Loading State -->
<div id="invoiceLoading" class="text-center py-5 d-none">
<div class="spinner"></div>
<p class="mt-3">Fetching invoices...</p>
</div>
<!-- No Invoices State -->
<div id="noInvoices" class="alert alert-info d-none">
<i class="fas fa-info-circle me-2"></i>
No unpaid invoices found. You can still process a payment.
</div>
<!-- Invoices List -->
<div id="invoicesList" class="d-none">
<div class="alert alert-info mb-4">
<i class="fas fa-info-circle me-2"></i>
Select which invoices this payment should be applied to. You can select multiple invoices or skip to process a general payment.
</div>
<div class="card bg-light">
<div class="card-body">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllInvoices" onchange="toggleAllInvoices()">
<label class="form-check-label" for="selectAllInvoices">
<strong>Select All Invoices</strong>
</label>
</div>
<hr>
<div id="invoicesContainer">
<!-- Invoice checkboxes populated by JavaScript -->
</div>
<div class="mt-4">
<label class="form-label">Total Selected Amount</label>
<p class="h4" id="selectedInvoicesTotal">$0.00</p>
</div>
</div>
</div> <!-- /.card -->
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-light" onclick="goBackToStep1()">
<span class="icon"><i class="fas fa-arrow-left"></i></span>
<span>Back</span>
</button>
</div>
<div class="control">
<button class="button is-primary" onclick="goToStep3()">
<span class="icon"><i class="fas fa-arrow-right"></i></span>
<span>Continue to Payment</span>
</button>
</div>
</div>
</div>
<!-- Step 3: Confirm Customer & Enter Amount -->
<div id="step3" class="payment-step d-none">
<h2 class="h4">
<i class="fas fa-user-check me-2"></i>
Confirm Customer & Payment Details
</h2>
<div class="box has-background-light mb-5">
<h3 class="subtitle is-5">Customer Information</h3>
<div class="card bg-light mb-5">
<div class="card-body">
<h3 class="h5">Customer Information</h3>
<div id="customerDetails">
<!-- Customer details will be populated here -->
</div>
</div>
</div> <!-- /.card -->
<form id="paymentForm">
<input type="hidden" id="confirmed_splynx_id" name="splynx_id">
@ -99,22 +217,22 @@
<i class="fas fa-credit-card"></i>
</span>
</div>
<div id="payment_method_error" class="notification is-danger is-light is-hidden mt-2">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
<div id="payment_method_error" class="alert alert-danger d-none mt-2">
<i class="fas fa-exclamation-triangle me-2"></i>
<span>Unable to load payment methods. Customer may not have any valid payment methods.</span>
</div>
<p class="help">Select which payment method to use for this payment</p>
<p class="form-text">Select which payment method to use for this payment</p>
</div>
<div class="notification is-info is-light">
<span class="icon"><i class="fas fa-info-circle"></i></span>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
This payment will be processed immediately using the selected payment method.
</div>
</form>
<div class="field is-grouped">
<div class="control">
<button class="button is-light" id="backBtn" onclick="goBackToStep1()">
<button class="button is-light" id="backBtn" onclick="goBackToStep2()">
<span class="icon"><i class="fas fa-arrow-left"></i></span>
<span>Back</span>
</button>
@ -128,144 +246,113 @@
</div>
</div>
</div>
</div> <!-- /.card -->
<!-- Payment Confirmation Modal -->
<div class="modal" id="confirmationModal">
<div class="modal-background" onclick="hideModal('confirmationModal')"></div>
<div class="modal-card">
<header class="modal-card-head has-background-warning">
<p class="modal-card-title">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
Confirm Payment Processing
</p>
<button class="delete" aria-label="close" onclick="hideModal('confirmationModal')"></button>
</header>
<section class="modal-card-body">
<div class="content">
<p class="is-size-5 has-text-weight-semibold">Are you sure you want to process this payment?</p>
<div class="box has-background-light">
<div class="columns">
<div class="column is-one-third">
<strong>Customer:</strong><br>
<span id="confirmCustomerName">-</span>
</div>
<div class="column is-one-third">
<strong>Amount:</strong><br>
<span id="confirmAmount" class="has-text-weight-bold is-size-4">$0.00</span>
</div>
<div class="column is-one-third">
<strong>Payment Method:</strong><br>
<span id="confirmPaymentMethod" class="tag is-info">-</span>
</div>
</div>
</div>
<div class="notification is-warning is-light">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span>
<strong>Warning:</strong> This action cannot be undone. The payment will be charged immediately.
</div>
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="confirmationModalLabel">Confirm Payment Processing</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p><strong>Customer:</strong> <span id="confirmCustomerName"></span></p>
<p><strong>Amount:</strong> <span id="confirmAmount"></span></p>
<p><strong>Payment Method:</strong> <span id="confirmPaymentMethod"></span></p>
<p class="mb-0">Are you sure you want to process this payment?</p>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-danger" id="confirmPaymentBtn" onclick="processPayment()">
<span class="icon"><i class="fas fa-credit-card"></i></span>
<span>Confirm & Process Payment</span>
</button>
<button class="button" onclick="hideModal('confirmationModal')">Cancel</button>
</footer>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" id="confirmPaymentBtn" onclick="processPayment()">
<i class="fas fa-credit-card me-2"></i>Confirm Payment
</button>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal" id="successModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head has-background-success">
<p class="modal-card-title has-text-white">
<span class="icon"><i class="fas fa-check-circle"></i></span>
Payment Successful
</p>
</header>
<section class="modal-card-body">
<div class="has-text-centered py-4">
<span class="icon is-large has-text-success mb-4">
<i class="fas fa-check-circle fa-3x"></i>
</span>
<h3 class="title is-4">Payment Processed Successfully!</h3>
<div id="successMessage" class="content">
<!-- Success details will be populated here -->
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="successModalLabel">
<i class="fas fa-check-circle me-2"></i>Payment Successful
</h5>
</div>
<div class="modal-body">
<div class="text-center py-4">
<i class="fas fa-check-circle fa-3x text-success mb-4"></i>
<h3 class="h4">Payment Processed Successfully!</h3>
<div id="successMessage" class="mt-3">
<!-- Success details will be populated here -->
</div>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-center">
<button class="button is-primary" onclick="closeSuccessModal()">
<span class="icon"><i class="fas fa-check"></i></span>
<span>Close</span>
</button>
</footer>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary" onclick="closeSuccessModal()">
<i class="fas fa-check me-2"></i>Close
</button>
</div>
</div>
</div>
</div>
<!-- Fee Update Modal (Orange) -->
<div class="modal" id="feeUpdateModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head has-background-warning">
<p class="modal-card-title has-text-dark">
<span class="icon"><i class="fas fa-clock"></i></span>
Direct Debit Processing
</p>
</header>
<section class="modal-card-body">
<div class="has-text-centered py-4">
<span class="icon is-large has-text-warning mb-4">
<i class="fas fa-clock fa-3x"></i>
</span>
<h3 class="title is-4">Direct Debit is still being processed</h3>
<div class="content">
<p>Your Direct Debit payment is currently being processed by the bank. This can take a few minutes to complete.</p>
<p><strong>Please check back later or click the button below to view payment details.</strong></p>
<div class="modal fade" id="feeUpdateModal" tabindex="-1" aria-labelledby="feeUpdateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="feeUpdateModalLabel">
<i class="fas fa-clock me-2"></i>Direct Debit Processing
</h5>
</div>
<div class="modal-body">
<div class="text-center py-4">
<i class="fas fa-clock fa-3x text-warning mb-4"></i>
<h3 class="h4">Direct Debit is still being processed</h3>
<div class="mt-3">
<p>Your Direct Debit payment is currently being processed by the bank. This can take a few minutes to complete.</p>
<p><strong>Please check back later or click the button below to view payment details.</strong></p>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-center">
<button class="button is-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()">
<span class="icon"><i class="fas fa-eye"></i></span>
<span>View Payment Details</span>
</button>
</footer>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()">
<i class="fas fa-eye me-2"></i>View Payment Details
</button>
</div>
</div>
</div>
</div>
<!-- Error Modal -->
<div class="modal" id="errorModal">
<div class="modal-background" onclick="hideModal('errorModal')"></div>
<div class="modal-card">
<header class="modal-card-head has-background-danger">
<p class="modal-card-title has-text-white">
<span class="icon"><i class="fas fa-exclamation-circle"></i></span>
Payment Failed
</p>
<button class="delete" aria-label="close" onclick="hideModal('errorModal')"></button>
</header>
<section class="modal-card-body">
<div class="has-text-centered py-4">
<span class="icon is-large has-text-danger mb-4">
<i class="fas fa-exclamation-circle fa-3x"></i>
</span>
<h3 class="title is-4">Payment Processing Failed</h3>
<div id="errorDetails" class="content">
<!-- Error details will be populated here -->
<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="errorModalLabel">
<i class="fas fa-exclamation-circle me-2"></i>Payment Failed
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="text-center py-4">
<i class="fas fa-exclamation-circle fa-3x text-danger mb-4"></i>
<h3 class="h4">Payment Processing Failed</h3>
<div id="errorDetails" class="mt-3">
<!-- Error details will be populated here -->
</div>
</div>
</div>
</section>
<footer class="modal-card-foot is-justify-content-center">
<button class="button is-danger" onclick="hideModal('errorModal')">
<span class="icon"><i class="fas fa-times"></i></span>
<span>Close</span>
</button>
</footer>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Close
</button>
</div>
</div>
</div>
</div>
@ -290,7 +377,7 @@
transition: opacity 0.3s ease, transform 0.3s ease;
}
.payment-step.is-hidden {
.payment-step.d-none {
display: none;
}
@ -299,31 +386,20 @@
font-size: 1.5rem;
font-weight: 600;
}
/* Modal enhancements */
.modal-card-head.has-background-warning {
color: var(--plutus-charcoal);
}
.modal-card-head.has-background-success {
color: white;
}
.modal-card-head.has-background-danger {
color: white;
}
</style>
<script>
let currentCustomerData = null;
let currentPaymentId = null;
let allInvoices = [];
let selectedInvoices = [];
function fetchCustomerDetails() {
const splynxIdElement = document.getElementById('lookup_splynx_id');
const splynxId = splynxIdElement ? splynxIdElement.value : '';
// Clear previous errors
document.getElementById('customerError').classList.add('is-hidden');
document.getElementById('customerError').classList.add('d-none');
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
showError('Please enter a valid Splynx Customer ID');
@ -331,7 +407,7 @@ function fetchCustomerDetails() {
}
// Show loading state
document.getElementById('loading').classList.remove('is-hidden');
document.getElementById('loading').classList.remove('d-none');
document.getElementById('nextBtn').disabled = true;
const apiUrl = `/api/splynx/${splynxId.trim()}`;
@ -346,7 +422,7 @@ function fetchCustomerDetails() {
})
.then(data => {
// Hide loading
document.getElementById('loading').classList.add('is-hidden');
document.getElementById('loading').classList.add('d-none');
document.getElementById('nextBtn').disabled = false;
if (data && data.id) {
@ -359,7 +435,7 @@ function fetchCustomerDetails() {
})
.catch(error => {
console.error('Error fetching customer:', error);
document.getElementById('loading').classList.add('is-hidden');
document.getElementById('loading').classList.add('d-none');
document.getElementById('nextBtn').disabled = false;
showError(`Failed to fetch customer details: ${error.message}`);
});
@ -367,32 +443,32 @@ function fetchCustomerDetails() {
function displayCustomerDetails(customer) {
const detailsHtml = `
<div class="columns is-multiline">
<div class="column is-half">
<div class="row g-3">
<div class="col-md-6">
<strong>Name:</strong><br>
<span>${customer.name || 'N/A'}</span>
</div>
<div class="column is-half">
<div class="col-md-6">
<strong>Customer ID:</strong><br>
<span class="tag is-info">${customer.id}</span>
<span class="badge bg-info">${customer.id}</span>
</div>
<div class="column is-half">
<div class="col-md-6">
<strong>Status:</strong><br>
${customer.status === 'active'
? '<span class="tag is-success">Active</span>'
: `<span class="tag is-warning">${customer.status || 'Unknown'}</span>`
? '<span class="badge bg-success">Active</span>'
: `<span class="badge bg-warning">${customer.status || 'Unknown'}</span>`
}
</div>
<div class="column is-half">
<div class="col-md-6">
<strong>Email:</strong><br>
<span>${customer.email || 'N/A'}</span>
</div>
<div class="column is-full">
<div class="col-12">
<strong>Address:</strong><br>
<span>${customer.street_1 || ''} ${customer.street_2 || ''}<br>
${customer.city || ''} ${customer.zip_code || ''}</span>
</div>
<div class="column is-half">
<div class="col-md-6">
<strong>Phone:</strong><br>
<span>${customer.phone || 'N/A'}</span>
</div>
@ -404,11 +480,14 @@ function displayCustomerDetails(customer) {
// Fetch payment methods for this customer
fetchPaymentMethods(customer.id);
// Fetch invoices for this customer
fetchInvoices(customer.id);
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
document.getElementById('customerError').classList.remove('is-hidden');
document.getElementById('customerError').classList.remove('d-none');
}
function fetchPaymentMethods(splynxId) {
@ -475,7 +554,7 @@ function displayPaymentMethods(paymentMethods) {
`;
// Hide any error messages
document.getElementById('payment_method_error').classList.add('is-hidden');
document.getElementById('payment_method_error').classList.add('d-none');
}
function showPaymentMethodError() {
@ -494,29 +573,23 @@ function showPaymentMethodError() {
`;
// Show error notification
document.getElementById('payment_method_error').classList.remove('is-hidden');
document.getElementById('payment_method_error').classList.remove('d-none');
}
function goToStep2() {
// Hide step 1, show step 2
document.getElementById('step1').classList.add('is-hidden');
document.getElementById('step2').classList.remove('is-hidden');
// Focus on amount input
document.getElementById('payment_amount').focus();
// Hide step 1, show step 2 (invoice selection)
document.getElementById('step1').classList.add('d-none');
document.getElementById('step2').classList.remove('d-none');
}
function goBackToStep1() {
// Show step 1, hide step 2
document.getElementById('step1').classList.remove('is-hidden');
document.getElementById('step2').classList.add('is-hidden');
document.getElementById('step1').classList.remove('d-none');
document.getElementById('step2').classList.add('d-none');
// Clear any errors
document.getElementById('customerError').classList.add('is-hidden');
document.getElementById('payment_method_error').classList.add('is-hidden');
// Clear form
document.getElementById('payment_amount').value = '';
document.getElementById('customerError').classList.add('d-none');
document.getElementById('payment_method_error').classList.add('d-none');
// Reset payment method selector to loading state
const container = document.getElementById('payment_method_container');
@ -532,6 +605,95 @@ function goBackToStep1() {
`;
}
function goBackToStep2() {
// Show step 2, hide step 3
document.getElementById('step3').classList.add('d-none');
document.getElementById('step2').classList.remove('d-none');
}
function fetchInvoices(splynxId) {
document.getElementById('invoiceLoading').classList.remove('d-none');
fetch(`/api/splynx/invoices/${splynxId}`)
.then(response => response.json())
.then(data => {
document.getElementById('invoiceLoading').classList.add('d-none');
if (data.success && data.invoices && data.invoices.length > 0) {
allInvoices = data.invoices;
displayInvoices(data.invoices);
} else {
document.getElementById('noInvoices').classList.remove('d-none');
}
})
.catch(error => {
console.error('Error fetching invoices:', error);
document.getElementById('invoiceLoading').classList.add('d-none');
document.getElementById('noInvoices').classList.remove('d-none');
});
}
function displayInvoices(invoices) {
const container = document.getElementById('invoicesContainer');
let html = '';
invoices.forEach(invoice => {
html += `
<div class="form-check">
<input class="form-check-input invoice-checkbox" type="checkbox"
value="${invoice.id}"
data-amount="${invoice.total}"
onchange="updateSelectedTotal()"
id="invoice-${invoice.id}">
<label class="form-check-label" for="invoice-${invoice.id}">
<strong>${invoice.number}</strong> - ${invoice.date} -
<span class="fw-bold">$${invoice.total.toFixed(2)}</span>
<br>
<span class="text-muted small">${invoice.description}</span>
</label>
</div>
`;
});
container.innerHTML = html;
document.getElementById('invoicesList').classList.remove('d-none');
}
function toggleAllInvoices() {
const selectAll = document.getElementById('selectAllInvoices').checked;
document.querySelectorAll('.invoice-checkbox').forEach(cb => cb.checked = selectAll);
updateSelectedTotal();
}
function updateSelectedTotal() {
const checkboxes = document.querySelectorAll('.invoice-checkbox:checked');
selectedInvoices = [];
let total = 0;
checkboxes.forEach(cb => {
selectedInvoices.push(cb.value);
total += parseFloat(cb.dataset.amount);
});
document.getElementById('selectedInvoicesTotal').textContent = `$${total.toFixed(2)}`;
}
function goToStep3() {
// Hide step 2, show step 3
document.getElementById('step2').classList.add('d-none');
document.getElementById('step3').classList.remove('d-none');
// Pre-fill amount with selected invoice total if any selected
if (selectedInvoices.length > 0) {
const total = Array.from(document.querySelectorAll('.invoice-checkbox:checked'))
.reduce((sum, cb) => sum + parseFloat(cb.dataset.amount), 0);
document.getElementById('payment_amount').value = total.toFixed(2);
}
// Focus on amount input
document.getElementById('payment_amount').focus();
}
function showConfirmationModal() {
const amount = document.getElementById('payment_amount').value;
const paymentMethodSelect = document.getElementById('payment_method_select');
@ -556,14 +718,19 @@ function showConfirmationModal() {
document.getElementById('confirmAmount').textContent = `$${parseFloat(amount).toFixed(2)}`;
document.getElementById('confirmPaymentMethod').textContent = paymentMethodSelect.options[paymentMethodSelect.selectedIndex].text;
// Show modal
document.getElementById('confirmationModal').classList.add('is-active');
// Show modal using Bootstrap Modal API
const modalEl = document.getElementById('confirmationModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
function processPayment() {
const form = document.getElementById('paymentForm');
const formData = new FormData(form);
// Add selected invoice IDs
formData.append('invoice_ids', selectedInvoices.join(','));
// Disable confirm button and show loading
const confirmBtn = document.getElementById('confirmPaymentBtn');
const originalText = confirmBtn.innerHTML;
@ -631,21 +798,36 @@ function showSuccessModal(data) {
`;
document.getElementById('successMessage').innerHTML = successHtml;
document.getElementById('successModal').classList.add('is-active');
// Show modal using Bootstrap Modal API
const modalEl = document.getElementById('successModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
function showErrorModal(errorMessage) {
document.getElementById('errorDetails').innerHTML = `<p>${errorMessage}</p>`;
document.getElementById('errorModal').classList.add('is-active');
// Show modal using Bootstrap Modal API
const modalEl = document.getElementById('errorModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
function hideModal(modalId) {
document.getElementById(modalId).classList.remove('is-active');
const el = document.getElementById(modalId);
if (!el) return;
const modal = bootstrap.Modal.getInstance(el) || bootstrap.Modal.getOrCreateInstance(el);
modal.hide();
}
function showFeeUpdateModal(data) {
currentPaymentId = data.payment_id;
document.getElementById('feeUpdateModal').classList.add('is-active');
// Show modal using Bootstrap Modal API
const modalEl = document.getElementById('feeUpdateModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
function viewPaymentDetails() {
@ -660,16 +842,13 @@ function closeSuccessModal() {
// Reset form to step 1
goBackToStep1();
document.getElementById('lookup_splynx_id').value = '';
document.getElementById('payment_amount').value = '';
currentCustomerData = null;
allInvoices = [];
selectedInvoices = [];
}
// Close modals on escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const activeModals = document.querySelectorAll('.modal.is-active');
activeModals.forEach(modal => modal.classList.remove('is-active'));
}
});
// Bootstrap modals handle Escape key automatically, no need for custom handler
// Enter key navigation
document.getElementById('lookup_splynx_id').addEventListener('keypress', function(event) {

1185
templates/main/single_payment_detail.html

File diff suppressed because it is too large

1029
templates/main/single_payments_list.html

File diff suppressed because it is too large

227
templates/search/search.html

@ -4,6 +4,63 @@
{% block head %}
<style>
/* Background styling */
body {
background-color: #3a3a3a !important;
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(58, 58, 58, 0.85);
z-index: -1;
}
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.search-container {
max-width: 1200px;
margin: 0 auto;
@ -20,15 +77,15 @@
.search-input {
font-size: 1.2rem;
padding: 1rem;
border: 2px solid #dbdbdb;
border: 2px solid #dee2e6;
border-radius: 6px;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: #3273dc;
border-color: #0d6efd;
outline: none;
box-shadow: 0 0 0 3px rgba(50, 115, 220, 0.25);
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
}
.search-results {
@ -53,7 +110,7 @@
.result-header {
display: flex;
justify-content: between;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
@ -73,7 +130,7 @@
}
.result-type.batch {
background-color: #3273dc;
background-color: #0d6efd;
color: white;
}
@ -136,61 +193,49 @@
{% block content %}
<div class="search-container">
<div class="search-box">
<h1 class="title is-2">🔍 Payment Search</h1>
<p class="subtitle">Search across all payment records by Splynx ID or Payment Intent</p>
<div class="field has-addons">
<div class="control is-expanded">
<input
class="input search-input"
type="text"
id="searchQuery"
placeholder="Enter Splynx ID (e.g. 123456) or Payment Intent (e.g. pi_1234567890)"
autocomplete="off"
>
</div>
<div class="control">
<button class="button is-primary is-large" onclick="performSearch()">
<span class="icon">
<i class="fas fa-search"></i>
</span>
<span>Search</span>
</button>
</div>
<h1 class="h2 mb-2">🔍 Payment Search</h1>
<p class="text-muted mb-4">Search across all payment records by Splynx ID or Payment Intent</p>
<div class="input-group input-group-lg mb-3">
<input
class="form-control search-input"
type="text"
id="searchQuery"
placeholder="Enter Splynx ID (e.g. 123456) or Payment Intent (e.g. pi_1234567890)"
autocomplete="off"
>
<button class="btn btn-primary" type="button" onclick="performSearch()">
<i class="fas fa-search"></i> Search
</button>
</div>
<div class="field is-grouped is-grouped-multiline" style="margin-top: 1rem;">
<div class="control">
<label class="label is-small">Search Type:</label>
<div class="select is-small">
<select id="searchType">
<option value="all">Auto-detect</option>
<option value="splynx_id">Splynx ID</option>
<option value="payment_intent">Payment Intent</option>
</select>
</div>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label form-label-sm">Search Type:</label>
<select class="form-select form-select-sm" id="searchType">
<option value="all">Auto-detect</option>
<option value="splynx_id">Splynx ID</option>
<option value="payment_intent">Payment Intent</option>
</select>
</div>
<div class="control">
<label class="label is-small">Results Limit:</label>
<div class="select is-small">
<select id="resultsLimit">
<option value="25">25 results</option>
<option value="50" selected>50 results</option>
<option value="100">100 results</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label form-label-sm">Results Limit:</label>
<select class="form-select form-select-sm" id="resultsLimit">
<option value="25">25 results</option>
<option value="50" selected>50 results</option>
<option value="100">100 results</option>
</select>
</div>
<div class="control">
<button class="button is-small is-info is-outlined" onclick="clearSearch()">
<span class="icon"><i class="fas fa-times"></i></span>
<span>Clear</span>
<div class="col-md-4 d-flex align-items-end">
<button class="btn btn-sm btn-outline-info" onclick="clearSearch()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
<div class="search-tips">
<h5 class="title is-6">💡 Search Tips:</h5>
<ul>
<h5 class="h6 mb-2">💡 Search Tips:</h5>
<ul class="mb-0">
<li><strong>Splynx ID:</strong> Enter customer ID number (e.g., 123456)</li>
<li><strong>Payment Intent:</strong> Enter full Stripe Payment Intent ID (e.g., pi_1234567890)</li>
<li><strong>Auto-detect:</strong> System automatically detects search type based on format</li>
@ -261,8 +306,10 @@ function showLoading() {
resultsDiv.innerHTML = `
<div class="search-results">
<div class="loading">
<div class="loader"></div>
<p>Searching payments...</p>
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Searching payments...</p>
</div>
</div>
`;
@ -272,7 +319,7 @@ function showError(message) {
const resultsDiv = document.getElementById('searchResults');
resultsDiv.style.display = 'block';
resultsDiv.innerHTML = `
<div class="search-error">
<div class="alert alert-danger" role="alert">
<strong>Search Error:</strong> ${message}
</div>
`;
@ -285,9 +332,9 @@ function displayResults(data) {
resultsDiv.innerHTML = `
<div class="search-results">
<div class="no-results">
<h3 class="title is-4">No Results Found</h3>
<h3 class="h4 mb-3">No Results Found</h3>
<p>No payments found matching "${data.search_query}"</p>
<p class="has-text-grey">Try searching with a different Splynx ID or Payment Intent</p>
<p class="text-muted">Try searching with a different Splynx ID or Payment Intent</p>
</div>
</div>
`;
@ -296,9 +343,9 @@ function displayResults(data) {
let resultsHtml = `
<div class="search-results">
<div style="padding: 1rem; border-bottom: 1px solid #f5f5f5; background-color: #f9f9f9;">
<h3 class="title is-4">Search Results</h3>
<p>Found ${data.total_found} payment(s) for "${data.search_query}" (${data.search_type})</p>
<div class="p-3 border-bottom bg-light">
<h3 class="h4 mb-2">Search Results</h3>
<p class="mb-0">Found ${data.total_found} payment(s) for "${data.search_query}" (${data.search_type})</p>
</div>
`;
@ -311,9 +358,9 @@ function displayResults(data) {
}
function createResultItem(result) {
const statusClass = result.success === true ? 'has-background-success-light' :
result.success === false ? 'has-background-danger-light' :
'has-background-warning-light';
const statusClass = result.success === true ? 'bg-success bg-opacity-10' :
result.success === false ? 'bg-danger bg-opacity-10' :
'bg-warning bg-opacity-10';
const statusText = result.success === true ? 'Success' :
result.success === false ? 'Failed' :
@ -328,16 +375,16 @@ function createResultItem(result) {
<div class="result-header">
<div>
<span class="result-type ${result.type}">${result.type}</span>
<strong style="margin-left: 0.5rem;">Payment #${result.id}</strong>
${result.batch_id ? `<span class="tag is-info is-light">Batch #${result.batch_id}</span>` : ''}
<strong class="ms-2">Payment #${result.id}</strong>
${result.batch_id ? `<span class="badge bg-info ms-2">Batch #${result.batch_id}</span>` : ''}
</div>
<div class="tags">
<span class="tag ${result.success === true ? 'is-success' : result.success === false ? 'is-danger' : 'is-warning'}">
<i class="fas ${statusIcon}"></i>&nbsp;${statusText}
<div>
<span class="badge ${result.success === true ? 'bg-success' : result.success === false ? 'bg-danger' : 'bg-warning'}">
<i class="fas ${statusIcon}"></i> ${statusText}
</span>
${result.refund ? '<span class="tag is-purple">Refunded</span>' : ''}
${result.pi_followup ? '<span class="tag is-warning">PI Follow-up</span>' : ''}
${result.refund_followup ? '<span class="tag is-info">Refund Follow-up</span>' : ''}
${result.refund ? '<span class="badge bg-purple ms-1">Refunded</span>' : ''}
${result.pi_followup ? '<span class="badge bg-warning ms-1">PI Follow-up</span>' : ''}
${result.refund_followup ? '<span class="badge bg-info ms-1">Refund Follow-up</span>' : ''}
</div>
</div>
@ -354,7 +401,7 @@ function createResultItem(result) {
</div>
<div class="result-field">
<strong>Payment Intent:</strong>
<code style="font-size: 0.8rem;">${result.payment_intent || 'N/A'}</code>
<code class="small">${result.payment_intent || 'N/A'}</code>
</div>
<div class="result-field">
<strong>Created:</strong> ${new Date(result.created).toLocaleDateString()} ${new Date(result.created).toLocaleTimeString()}
@ -365,26 +412,23 @@ function createResultItem(result) {
</div>
${result.error ? `
<div class="notification is-danger is-light" style="margin-top: 0.5rem;">
<div class="alert alert-danger alert-sm mt-2">
<strong>Error:</strong> ${result.error.substring(0, 200)}${result.error.length > 200 ? '...' : ''}
</div>
` : ''}
<div class="result-actions">
<a href="${result.detail_url}" class="button is-small is-primary">
<span class="icon"><i class="fas fa-eye"></i></span>
<span>View Details</span>
<a href="${result.detail_url}" class="btn btn-sm btn-primary">
<i class="fas fa-eye"></i> View Details
</a>
${result.batch_url ? `
<a href="${result.batch_url}" class="button is-small is-info">
<span class="icon"><i class="fas fa-layer-group"></i></span>
<span>View Batch</span>
<a href="${result.batch_url}" class="btn btn-sm btn-info">
<i class="fas fa-layer-group"></i> View Batch
</a>
` : ''}
${result.splynx_url ? `
<a href="${result.splynx_url}" target="_blank" class="button is-small is-link">
<span class="icon"><i class="fas fa-external-link-alt"></i></span>
<span>Splynx Customer</span>
<a href="${result.splynx_url}" target="_blank" class="btn btn-sm btn-link">
<i class="fas fa-external-link-alt"></i> Splynx Customer
</a>
` : ''}
</div>
@ -400,26 +444,11 @@ function clearSearch() {
document.getElementById('searchQuery').focus();
}
// CSS for loader
// Purple badge custom style
const style = document.createElement('style');
style.textContent = `
.loader {
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.tag.is-purple {
background-color: #9370db;
.bg-purple {
background-color: #9370db !important;
color: white;
}
`;

Loading…
Cancel
Save