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 from flask_login import LoginManager
import pymysql import pymysql
import os import os
import json
from config import Config from config import Config
db = SQLAlchemy() db = SQLAlchemy()
@ -85,6 +86,23 @@ def create_app():
'get_user_permission_level': get_user_permission_level '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 # Note: Database tables will be managed by Flask-Migrate
# Use 'flask db init', 'flask db migrate', 'flask db upgrade' commands # 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 json
import pymysql 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 app import db
from typing import Dict, Any, List
from models import PaymentBatch, Payments, SinglePayments, PaymentPlans, Logs, Users from models import PaymentBatch, Payments, SinglePayments, PaymentPlans, Logs, Users
from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET
from stripe_payment_processor import StripePaymentProcessor 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)}") print(f"processPaymentResult error: {e}\n{json.dumps(result)}")
payment.PI_FollowUp = True 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.""" """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") #result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid")
params = { print(f"\n\nInvoice IDs to Pay: {invoice_ids} of type {type(invoice_ids)}\n")
'main_attributes': {
'customer_id': splynx_id, #params = {
'status': ['IN', ['not_paid', 'pending']] # '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}") #}
#query_string = splynx.build_splynx_query_params(params)
#result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pay = { invoice_pay = {
"status": "paid" "status": "paid",
"payment_id": splynx_pay_id,
"date_payment": datetime.now().strftime("%Y-%m-%d")
} }
for pay in result: #for pay in result:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) for invoice in invoice_ids:
return res 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.""" """Mark Splynx invoices as pending for the given customer ID."""
params = { #params = {
'main_attributes': { # 'main_attributes': {
'customer_id': splynx_id, # 'customer_id': splynx_id,
'status': 'not_paid' # 'status': 'not_paid'
}, # },
} #}
query_string = splynx.build_splynx_query_params(params) #query_string = splynx.build_splynx_query_params(params)
result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") #result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}")
invoice_pending = { invoice_pending = {
"status": "pending" "status": "pending"
} }
updated_invoices = [] updated_invoices = []
for invoice in result: for invoice in invoice_list:
res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pending) res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pending)
if res: if res:
updated_invoices.append(res) updated_invoices.append(res)
return updated_invoices 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.""" """Add a payment record to Splynx."""
from datetime import datetime from datetime import datetime
@ -359,7 +366,8 @@ def add_payment_splynx(splynx_id, pi_id, pay_id, amount):
"amount": amount, "amount": amount,
"date": str(datetime.now().strftime('%Y-%m-%d')), "date": str(datetime.now().strftime('%Y-%m-%d')),
"field_1": pi_id, "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) 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() payment = db.session.query(Payments).filter(Payments.id == payment_id).first()
if not payment: 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')) return redirect(url_for('main.batch_list'))
# Log the payment detail view access # 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}" 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']) @main_bp.route('/single-payment/check-intent/<int:payment_id>', methods=['POST'])
@login_required @login_required
@ -814,6 +828,37 @@ def check_payment_intent(payment_id):
print(f"Check payment intent error: {e}") print(f"Check payment intent error: {e}")
return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500 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']) @main_bp.route('/single-payment/process', methods=['POST'])
@helpdesk_required @helpdesk_required
def process_single_payment(): def process_single_payment():
@ -823,6 +868,8 @@ def process_single_payment():
splynx_id = request.form.get('splynx_id') splynx_id = request.form.get('splynx_id')
amount = request.form.get('amount') amount = request.form.get('amount')
payment_method = request.form.get('payment_method') 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 # Validate inputs
if not splynx_id or not amount or not payment_method: if not splynx_id or not amount or not payment_method:
@ -852,7 +899,8 @@ def process_single_payment():
Splynx_ID=splynx_id, Splynx_ID=splynx_id,
Stripe_Customer_ID=stripe_customer_id, Stripe_Customer_ID=stripe_customer_id,
Payment_Amount=amount, Payment_Amount=amount,
Who=current_user.id Who=current_user.id,
Invoices_to_Pay=invoice_ids
) )
db.session.add(payment_record) db.session.add(payment_record)
db.session.commit() # Commit to get the payment ID db.session.commit() # Commit to get the payment ID
@ -982,18 +1030,20 @@ def process_single_payment():
if result.get('needs_fee_update'): if result.get('needs_fee_update'):
payment_record.PI_FollowUp = True payment_record.PI_FollowUp = True
# Mark invoices as pending when PI_FollowUp is set # Mark invoices as pending when PI_FollowUp is set
if Config.PROCESS_LIVE: #if Config.PROCESS_LIVE:
try: try:
find_set_pending_splynx_invoices(splynx_id) pending_invoices = find_set_pending_splynx_invoices(splynx_id, invoice_list)
except Exception as e: if invoice_list[0] == 0:
print(f"⚠️ Error setting invoices to pending: {e}") 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": if result.get('payment_method_type') == "card":
payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card') payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card')
elif result.get('payment_method_type') == "au_becs_debit": elif result.get('payment_method_type') == "au_becs_debit":
payment_record.Payment_Method = result['payment_method_type'] 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'] payment_record.Fee_Total = result['fee_details']['total_fee']
for fee_type in result['fee_details']['fee_breakdown']: for fee_type in result['fee_details']['fee_breakdown']:
if fee_type['type'] == "tax": if fee_type['type'] == "tax":
@ -1007,27 +1057,27 @@ def process_single_payment():
# Check if payment was actually successful # Check if payment was actually successful
if result.get('success'): if result.get('success'):
# Payment succeeded - update Splynx if in live mode # Payment succeeded - update Splynx if in live mode
if Config.PROCESS_LIVE: #if Config.PROCESS_LIVE:
try: try:
# Mark invoices as paid in Splynx # Add payment record to Splynx
find_pay_splynx_invoices(splynx_id) splynx_payment_id = add_payment_splynx(
splynx_id=splynx_id,
# Add payment record to Splynx pi_id=result.get('payment_intent_id'),
splynx_payment_id = add_payment_splynx( pay_id=payment_record.id,
splynx_id=splynx_id, amount=amount,
pi_id=result.get('payment_intent_id'), invoice_id=invoice_list[0]
pay_id=payment_record.id, )
amount=amount
)
if splynx_payment_id: if splynx_payment_id:
print(f"✅ Splynx payment record created: {splynx_payment_id}") print(f"✅ Splynx payment record created: {splynx_payment_id}")
else: # Mark invoices as paid in Splynx
print("⚠️ Failed to create Splynx payment record") find_pay_splynx_invoices(splynx_id, splynx_payment_id, invoice_list)
else:
print("⚠️ Failed to create Splynx payment record")
except Exception as splynx_error: except Exception as splynx_error:
print(f"❌ Error updating Splynx: {splynx_error}") print(f"❌ Error updating Splynx: {splynx_error}")
# Continue processing even if Splynx update fails # Continue processing even if Splynx update fails
# Log successful payment # Log successful payment
log_activity( 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' SECRET_KEY = os.environ.get('SECRET_KEY') or 'plutus-dev-secret-key-change-in-production'
# PostgreSQL database configuration (Flask-SQLAlchemy) # PostgreSQL database configuration (Flask-SQLAlchemy)
#SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/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_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus'
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
@ -37,7 +37,7 @@ class Config:
# Threading configuration # Threading configuration
MAX_PAYMENT_THREADS = 15 # Number of concurrent payment processing threads 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 API Keys
STRIPE_LIVE_API_KEY = os.environ.get('STRIPE_LIVE_API_KEY') or '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' 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): class PaymentBatch(db.Model):
__tablename__ = 'PaymentBatch' __tablename__ = 'PaymentBatch'
id = db.Column(db.Integer, primary_key=True) 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): class Payments(db.Model):
@ -58,7 +58,7 @@ class Payments(db.Model):
Refund_JSON = db.Column(db.Text()) Refund_JSON = db.Column(db.Text())
Stripe_Refund_ID = db.Column(db.String()) Stripe_Refund_ID = db.Column(db.String())
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True) 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) PaymentPlan_ID = db.Column(db.Integer, db.ForeignKey('PaymentPlans.id'), nullable=True)
Invoices_to_Pay = db.Column(db.String()) Invoices_to_Pay = db.Column(db.String())
@ -86,7 +86,7 @@ class SinglePayments(db.Model):
Refund_JSON = db.Column(db.Text()) Refund_JSON = db.Column(db.Text())
Stripe_Refund_ID = db.Column(db.String()) Stripe_Refund_ID = db.Column(db.String())
Stripe_Refund_Created = db.Column(db.DateTime, nullable=True) 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) Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False)
Invoices_to_Pay = db.Column(db.String()) Invoices_to_Pay = db.Column(db.String())
@ -111,6 +111,6 @@ class PaymentPlans(db.Model):
Frequency = db.Column(db.String(50)) Frequency = db.Column(db.String(50))
Start_Date = db.Column(db.DateTime, nullable=True) Start_Date = db.Column(db.DateTime, nullable=True)
Stripe_Payment_Method = db.Column(db.String(50)) 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) Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False)
Enabled = db.Column(db.Boolean, nullable=True, default=True) Enabled = db.Column(db.Boolean, nullable=True, default=True)

2
payment_processors/batch_processor.py

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

164
templates/auth/add_user.html

@ -2,74 +2,120 @@
{% block title %}Add User - Plutus{% endblock %} {% 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 %} {% block content %}
<div class="columns is-centered"> <div class="row justify-content-center">
<div class="column is-6"> <div class="col-md-6">
<div class="box"> <div class="card shadow">
<h1 class="title">Add New User</h1> <div class="card-body">
<h1 class="h3 mb-4">Add New User</h1>
<form method="POST">
<div class="field"> <form method="POST">
<label class="label">Username</label> <div class="mb-3">
<div class="control has-icons-left"> <label for="username" class="form-label">Username</label>
<input class="input" type="text" name="username" placeholder="Username" required> <div class="input-group">
<span class="icon is-small is-left"> <span class="input-group-text"><i class="fas fa-user"></i></span>
<i class="fas fa-user"></i> <input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
</span> </div>
</div> </div>
</div>
<div class="mb-3">
<div class="field"> <label for="full_name" class="form-label">Full Name</label>
<label class="label">Full Name</label> <div class="input-group">
<div class="control has-icons-left"> <span class="input-group-text"><i class="fas fa-id-card"></i></span>
<input class="input" type="text" name="full_name" placeholder="Full Name" required> <input type="text" class="form-control" id="full_name" name="full_name" placeholder="Full Name" required>
<span class="icon is-small is-left"> </div>
<i class="fas fa-id-card"></i>
</span>
</div> </div>
</div>
<div class="mb-3">
<div class="field"> <label for="email" class="form-label">Email</label>
<label class="label">Email</label> <div class="input-group">
<div class="control has-icons-left"> <span class="input-group-text"><i class="fas fa-envelope"></i></span>
<input class="input" type="email" name="email" placeholder="Email Address" required> <input type="email" class="form-control" id="email" name="email" placeholder="Email Address" required>
<span class="icon is-small is-left"> </div>
<i class="fas fa-envelope"></i>
</span>
</div> </div>
</div>
<div class="mb-3">
<div class="field"> <label for="password" class="form-label">Password</label>
<label class="label">Password</label> <div class="input-group">
<div class="control has-icons-left"> <span class="input-group-text"><i class="fas fa-lock"></i></span>
<input class="input" type="password" name="password" placeholder="Password" required> <input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
<span class="icon is-small is-left"> </div>
<i class="fas fa-lock"></i>
</span>
</div> </div>
</div>
<div class="field"> <div class="mb-3">
<label class="label">Permissions</label> <label for="permissions" class="form-label">Permissions</label>
<div class="control"> <input type="text" class="form-control" id="permissions" name="permissions" placeholder="Permissions (optional)">
<input class="input" type="text" name="permissions" placeholder="Permissions (optional)">
</div> </div>
</div>
<div class="d-flex gap-2">
<div class="field is-grouped"> <button class="btn btn-primary" type="submit">
<div class="control"> <i class="fas fa-plus"></i> Add User
<button class="button is-primary" type="submit">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>Add User</span>
</button> </button>
<a class="btn btn-light" href="{{ url_for('auth.list_users') }}">Cancel</a>
</div> </div>
<div class="control"> </form>
<a class="button is-light" href="{{ url_for('auth.list_users') }}">Cancel</a> </div>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>

93
templates/auth/list_users.html

@ -2,24 +2,83 @@
{% block title %}Users - Plutus{% endblock %} {% 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 %} {% block content %}
<div class="level"> <div class="d-flex justify-content-between align-items-center mb-4">
<div class="level-left"> <h1 class="h2 mb-0">Users</h1>
<h1 class="title">Users</h1> <a class="btn btn-primary" href="{{ url_for('auth.add_user') }}">
</div> <i class="fas fa-plus"></i> Add User
<div class="level-right"> </a>
<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> </div>
{% if users %} {% if users %}
<div class="table-container"> <div class="table-responsive">
<table class="table is-fullwidth is-striped is-hoverable"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
@ -39,7 +98,7 @@
<td>{{ user.FullName }}</td> <td>{{ user.FullName }}</td>
<td>{{ user.Email }}</td> <td>{{ user.Email }}</td>
<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' }} {{ 'Active' if user.Enabled else 'Disabled' }}
</span> </span>
</td> </td>
@ -51,8 +110,8 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="notification is-info"> <div class="alert alert-info">
<p>No users found. <a href="{{ url_for('auth.add_user') }}">Add the first user</a>.</p> <p class="mb-0">No users found. <a href="{{ url_for('auth.add_user') }}" class="alert-link">Add the first user</a>.</p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

120
templates/auth/login.html

@ -2,44 +2,98 @@
{% block title %}Login - Plutus{% endblock %} {% 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 %} {% block content %}
<div class="columns is-centered"> <div class="row justify-content-center">
<div class="column is-4"> <div class="col-md-4">
<div class="box"> <div class="card shadow">
<h1 class="title has-text-centered">Login to Plutus</h1> <div class="card-body">
<h1 class="h3 text-center mb-4">Login to Plutus</h1>
<form method="POST">
<div class="field"> <form method="POST">
<label class="label">Username</label> <div class="mb-3">
<div class="control has-icons-left"> <label for="username" class="form-label">Username</label>
<input class="input" type="text" name="username" placeholder="Username" required> <div class="input-group">
<span class="icon is-small is-left"> <span class="input-group-text"><i class="fas fa-user"></i></span>
<i class="fas fa-user"></i> <input type="text" class="form-control" id="username" name="username" placeholder="Username" required>
</span> </div>
</div> </div>
</div>
<div class="mb-3">
<div class="field"> <label for="password" class="form-label">Password</label>
<label class="label">Password</label> <div class="input-group">
<div class="control has-icons-left"> <span class="input-group-text"><i class="fas fa-lock"></i></span>
<input class="input" type="password" name="password" placeholder="Password" required> <input type="password" class="form-control" id="password" name="password" placeholder="Password" required>
<span class="icon is-small is-left"> </div>
<i class="fas fa-lock"></i>
</span>
</div> </div>
</div>
<div class="d-grid">
<div class="field"> <button class="btn btn-primary" type="submit">
<div class="control"> <i class="fas fa-sign-in-alt"></i> Login
<button class="button is-primary is-fullwidth" type="submit">
<span class="icon">
<i class="fas fa-sign-in-alt"></i>
</span>
<span>Login</span>
</button> </button>
</div> </div>
</div> </form>
</form> </div>
</div> </div>
</div> </div>
</div> </div>

242
templates/base.html

@ -4,165 +4,123 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Plutus{% endblock %}</title> <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') }}"> <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> <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<nav class="navbar is-dark" role="navigation"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="navbar-brand"> <div class="container-fluid">
<a class="navbar-item" href="{{ url_for('main.index') }}"> <a class="navbar-brand" href="{{ url_for('main.index') }}">
<strong>Plutus</strong> <strong>Plutus</strong>
</a> </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="collapse navbar-collapse" id="navbarNav">
<div class="navbar-start"> <ul class="navbar-nav me-auto">
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a class="navbar-item" href="{{ url_for('main.index') }}"> <li class="nav-item">
Dashboard <a class="nav-link" href="{{ url_for('main.index') }}">Dashboard</a>
</a> </li>
{% if can_view_data() %} {% if can_view_data() %}
<a class="navbar-item" href="{{ url_for('search.search_page') }}"> <li class="nav-item">
<span class="icon"> <a class="nav-link" href="{{ url_for('search.search_page') }}">
<i class="fas fa-search"></i> <i class="fas fa-search"></i> Search Payments
</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>
<a class="navbar-item" href="{{ url_for('auth.add_user') }}"> </li>
Add User {% 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> </a>
</div> <ul class="dropdown-menu">
</div> <li><a class="dropdown-item" href="{{ url_for('auth.list_users') }}">List Users</a></li>
{% endif %} <li><a class="dropdown-item" href="{{ url_for('auth.add_user') }}">Add User</a></li>
{% if can_manage_batch_payments() %} </ul>
<a class="navbar-item" href="{{ url_for('main.batch_list') }}"> </li>
<span class="icon"> {% endif %}
<i class="fas fa-file-invoice-dollar"></i> {% if can_manage_batch_payments() %}
</span> <li class="nav-item">
<span>Payment Batches</span> <a class="nav-link" href="{{ url_for('main.batch_list') }}">
</a> <i class="fas fa-file-invoice-dollar"></i> Payment Batches
{% 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>
<a class="navbar-item" href="{{ url_for('main.single_payment') }}"> </li>
<span class="icon"> {% endif %}
<i class="fas fa-plus"></i> {% if can_process_single_payments() %}
</span> <li class="nav-item dropdown">
<span>New Payment</span> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-credit-card"></i> Single Payments
</a> </a>
<hr class="navbar-divider"> <ul class="dropdown-menu">
<a class="navbar-item" href="{{ url_for('main.add_payment_method') }}"> <li><a class="dropdown-item" href="{{ url_for('main.single_payments_list') }}"><i class="fas fa-list"></i> View Payments</a></li>
<span class="icon"> <li><a class="dropdown-item" href="{{ url_for('main.single_payment') }}"><i class="fas fa-plus"></i> New Payment</a></li>
<i class="fas fa-credit-card"></i> <li><hr class="dropdown-divider"></li>
</span> <li><a class="dropdown-item" href="{{ url_for('main.add_payment_method') }}"><i class="fas fa-credit-card"></i> Add Payment Method</a></li>
<span>Add Payment Method</span> </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> </a>
</div> <ul class="dropdown-menu">
</div> <li><a class="dropdown-item" href="{{ url_for('main.payment_plans_list') }}"><i class="fas fa-list"></i> View Plans</a></li>
{% endif %} <li><a class="dropdown-item" href="{{ url_for('main.payment_plans_create') }}"><i class="fas fa-plus"></i> New Plan</a></li>
{% if can_manage_payment_plans() %} </ul>
<div class="navbar-item has-dropdown is-hoverable"> </li>
<a class="navbar-link"> {% endif %}
<span class="icon"> {% if can_view_logs() %}
<i class="fas fa-calendar-alt"></i> <li class="nav-item">
</span> <a class="nav-link" href="{{ url_for('main.logs_list') }}">
<span>Payment Plans</span> <i class="fas fa-file-alt"></i> System Logs
</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>
<a class="navbar-item" href="{{ url_for('main.payment_plans_create') }}"> </li>
<span class="icon"> <li class="nav-item">
<i class="fas fa-plus"></i> <a class="nav-link" href="{{ url_for('analytics.dashboard') }}">
</span> <i class="fas fa-chart-line"></i> Analytics
<span>New Plan</span>
</a> </a>
</div> </li>
</div> {% endif %}
{% endif %} {% endif %}
{% if can_view_logs() %} </ul>
<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"> <ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable"> <li class="nav-item dropdown">
<a class="navbar-link"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
{{ current_user.FullName }}&nbsp;&nbsp; {{ current_user.FullName }}
{% set user_permission = current_user.Permissions or 'None' %} {% 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' }}"> <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 }} {{ user_permission }}
</span> </span>
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{{ url_for('auth.logout') }}">
Logout
</a> </a>
</div> <ul class="dropdown-menu dropdown-menu-end">
</div> <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
{% else %} </ul>
<div class="navbar-item"> </li>
<a class="button is-primary" href="{{ url_for('auth.login') }}"> {% else %}
Login <li class="nav-item">
</a> <a class="btn btn-primary" href="{{ url_for('auth.login') }}">Login</a>
</div> </li>
{% endif %} {% endif %}
</ul>
</div> </div>
</div> </div>
</nav> </nav>
<main class="section"> <main class="py-4">
<div class="container"> <div class="container">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
<div class="notification is-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }}"> <div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
<button class="delete"></button>
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@ -172,24 +130,14 @@
</div> </div>
</main> </main>
<footer class="footer"> <footer class="footer mt-5 py-3 bg-dark text-center">
<div class="content has-text-centered"> <div class="container">
<p> <p class="text-light mb-0">
<strong style="color: var(--plutus-gold);">Plutus</strong> - Payment Processing System <strong style="color: var(--plutus-gold);">Plutus</strong> - Payment Processing System
</p> </p>
</div> </div>
</footer> </footer>
<script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></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> </body>
</html> </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 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 %} {% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs"> <nav class="breadcrumb" aria-label="breadcrumbs">
<ul> <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 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 %} {% block content %}
<div class="level"> <div class="page-title text-center">
<div class="level-left"> <h1 class="h2 mb-0">Payment Batches</h1>
<h1 class="title">Payment Batches</h1>
</div>
</div> </div>
{% if batches %} {% if batches %}
<div class="table-container"> <div class="table-responsive">
<table class="table is-fullwidth is-striped is-hoverable"> <table class="table table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>Batch ID</th> <th>Batch ID</th>
@ -32,7 +108,7 @@
</td> </td>
<td>{{ batch.Created.strftime('%Y-%m-%d %H:%M') if batch.Created else '-' }}</td> <td>{{ batch.Created.strftime('%Y-%m-%d %H:%M') if batch.Created else '-' }}</td>
<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>
<td> <td>
<strong>{{ batch.total_amount | currency }}</strong> <strong>{{ batch.total_amount | currency }}</strong>
@ -44,41 +120,38 @@
{% if batch.payment_count and batch.payment_count > 0 %} {% if batch.payment_count and batch.payment_count > 0 %}
{% set success_rate = (batch.successful_count or 0) / batch.payment_count * 100 %} {% set success_rate = (batch.successful_count or 0) / batch.payment_count * 100 %}
{% if success_rate >= 90 %} {% 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 %} {% 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 %} {% else %}
<span class="tag is-danger">{{ "%.1f"|format(success_rate) }}%</span> <span class="badge bg-danger">{{ "%.1f"|format(success_rate) }}%</span>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="tag">0%</span> <span class="badge bg-secondary">0%</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
<div class="tags"> <div>
{% if batch.successful_count %} {% 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 %} {% endif %}
{% if batch.pending_count %} {% 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 %} {% endif %}
{% if batch.failed_count %} {% 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 %} {% endif %}
{% if batch.error_count %} {% 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 %} {% endif %}
{% if not batch.successful_count and not batch.failed_count %} {% 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 %} {% endif %}
</div> </div>
</td> </td>
<td> <td>
<a class="button is-primary is-small" href="{{ url_for('main.batch_detail', batch_id=batch.id) }}"> <a class="btn btn-primary btn-sm" href="{{ url_for('main.batch_detail', batch_id=batch.id) }}">
<span class="icon"> <i class="fas fa-eye"></i> View Details
<i class="fas fa-eye"></i>
</span>
<span>View Details</span>
</a> </a>
</td> </td>
</tr> </tr>
@ -87,8 +160,8 @@
</table> </table>
</div> </div>
{% else %} {% else %}
<div class="notification is-info"> <div class="alert alert-info">
<p>No payment batches found. <a href="{{ url_for('main.index') }}">Return to dashboard</a>.</p> <p class="mb-0">No payment batches found. <a href="{{ url_for('main.index') }}" class="alert-link">Return to dashboard</a>.</p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% 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 %} {% endblock %}
{% block content %} {% block content %}
<div class="hero is-primary"> <div class="hero text-center mb-4">
<div class="hero-body has-text-centered"> <h1 class="display-4 fw-bold">
<h1 class="title"> Welcome to Plutus
Welcome to Plutus </h1>
</h1> <p class="lead">
<h2 class="subtitle"> Payment Processing System
Payment Processing System </p>
</h2>
</div>
</div> </div>
<img src="{{ url_for('static', filename='images/plutus3.JPG') }}" alt="Plutus - God of Wealth" class="plutus-image"> <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="notification is-info"> <div class="alert alert-info">
<h4 class="title is-5">Welcome, {{ current_user.FullName }}!</h4> <h4 class="h5 mb-2">Welcome, {{ current_user.FullName }}!</h4>
<p>You are successfully logged into the Plutus payment processing system.</p> <p class="mb-0">You are successfully logged into the Plutus payment processing system.</p>
</div> </div>
{% endblock %} {% endblock %}

477
templates/main/logs_list.html

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

346
templates/main/payment_plans_list.html

@ -2,190 +2,242 @@
{% block title %}Payment Plans - Plutus{% endblock %} {% 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 %} {% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs"> <nav aria-label="breadcrumb">
<ul> <ol class="breadcrumb">
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li> <li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">Payment Plans</a></li> <li class="breadcrumb-item active" aria-current="page">Payment Plans</li>
</ul> </ol>
</nav> </nav>
<div class="level"> <div class="d-flex justify-content-between align-items-start mb-4">
<div class="level-left"> <div>
<div> <h1 class="h2 mb-1">Payment Plans</h1>
<h1 class="title">Payment Plans</h1> <p class="text-muted">Recurring payment management</p>
<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> </div>
<a class="btn btn-primary" href="{{ url_for('main.payment_plans_create') }}">
<i class="fas fa-plus"></i> New Payment Plan
</a>
</div> </div>
<!-- Summary Statistics --> <!-- Summary Statistics -->
<div class="columns"> <div class="row mb-4">
<div class="column is-3"> <div class="col-md-3">
<div class="box has-text-centered"> <div class="card shadow text-center">
<p class="title is-4 has-text-success">{{ summary.active_plans }}</p> <div class="card-body">
<p class="subtitle is-6">Active Plans</p> <p class="h4 text-success mb-1">{{ summary.active_plans }}</p>
<p class="text-muted mb-0">Active Plans</p>
</div>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="col-md-3">
<div class="box has-text-centered"> <div class="card shadow text-center">
<p class="title is-4 has-text-warning">{{ summary.inactive_plans }}</p> <div class="card-body">
<p class="subtitle is-6">Inactive Plans</p> <p class="h4 text-warning mb-1">{{ summary.inactive_plans }}</p>
<p class="text-muted mb-0">Inactive Plans</p>
</div>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="col-md-3">
<div class="box has-text-centered"> <div class="card shadow text-center">
<p class="title is-4 has-text-info">{{ summary.total_plans }}</p> <div class="card-body">
<p class="subtitle is-6">Total Plans</p> <p class="h4 text-info mb-1">{{ summary.total_plans }}</p>
<p class="text-muted mb-0">Total Plans</p>
</div>
</div> </div>
</div> </div>
<div class="column is-3"> <div class="col-md-3">
<div class="box has-text-centered"> <div class="card shadow text-center">
<p class="title is-4 has-text-primary">{{ summary.total_recurring_amount | currency }}</p> <div class="card-body">
<p class="subtitle is-6">Monthly Recurring</p> <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> </div>
</div> </div>
<!-- Payment Plans Table --> <!-- Payment Plans Table -->
<div class="box"> <div class="card shadow">
<div class="level"> <div class="card-body">
<div class="level-left"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="title is-4">Payment Plans</h2> <h2 class="h4 mb-0">Payment Plans</h2>
</div> <div class="input-group" style="max-width: 350px;">
<div class="level-right"> <span class="input-group-text"><i class="fas fa-search"></i></span>
<div class="field"> <input class="form-control" type="text" id="searchInput" placeholder="Search Customer ID, Amount...">
<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> </div>
</div> </div>
</div>
<!-- Filter Controls --> <!-- Filter Controls -->
<div class="field is-grouped is-grouped-multiline"> <div class="row g-3 mb-3">
<div class="control"> <div class="col-md-6">
<label class="label is-small">Filter by Status:</label> <label class="form-label form-label-sm">Filter by Status:</label>
<div class="select is-small"> <select class="form-select form-select-sm" id="statusFilter">
<select id="statusFilter">
<option value="">All</option> <option value="">All</option>
<option value="active">Active</option> <option value="active">Active</option>
<option value="inactive">Inactive</option> <option value="inactive">Inactive</option>
</select> </select>
</div> </div>
</div> <div class="col-md-6">
<div class="control"> <label class="form-label form-label-sm">Filter by Frequency:</label>
<label class="label is-small">Filter by Frequency:</label> <select class="form-select form-select-sm" id="frequencyFilter">
<div class="select is-small">
<select id="frequencyFilter">
<option value="">All</option> <option value="">All</option>
<option value="Weekly">Weekly</option> <option value="Weekly">Weekly</option>
<option value="Fortnightly">Fortnightly</option> <option value="Fortnightly">Fortnightly</option>
</select> </select>
</div> </div>
</div> </div>
</div>
{% if plans %} {% if plans %}
<div class="table-container"> <div class="table-responsive">
<table class="table is-fullwidth is-striped is-hoverable" id="plansTable"> <table class="table table-striped table-hover" id="plansTable">
<thead> <thead>
<tr> <tr>
<th>Plan ID</th> <th>Plan ID</th>
<th>Customer</th> <th>Customer</th>
<th>Splynx ID</th> <th>Splynx ID</th>
<th>Amount</th> <th>Amount</th>
<th>Frequency</th> <th>Frequency</th>
<th>Start Date</th> <th>Start Date</th>
<th>Status</th> <th>Status</th>
<th>Created</th> <th>Created</th>
<th>Created By</th> <th>Created By</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for plan in plans %} {% for plan in plans %}
<tr data-status="{{ 'active' if plan.Enabled else 'inactive' }}" <tr data-status="{{ 'active' if plan.Enabled else 'inactive' }}"
data-frequency="{{ plan.Frequency }}" data-frequency="{{ plan.Frequency }}"
data-splynx-id="{{ plan.Splynx_ID }}" data-splynx-id="{{ plan.Splynx_ID }}"
data-amount="{{ plan.Amount }}" data-amount="{{ plan.Amount }}"
data-customer-name=""> data-customer-name="">
<td> <td>
<a href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}" class="has-text-weight-semibold"> <a href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}" class="fw-semibold">
#{{ plan.id }} #{{ plan.id }}
</a> </a>
</td> </td>
<td> <td>
<span class="customer-name" data-splynx-id="{{ plan.Splynx_ID }}"> <span class="customer-name" data-splynx-id="{{ plan.Splynx_ID }}">
<span class="icon"><i class="fas fa-spinner fa-spin"></i></span> <i class="fas fa-spinner fa-spin"></i>
Loading... Loading...
</span> </span>
</td> </td>
<td> <td>
<a href="https://billing.interphone.com.au/admin/customers/view?id={{ plan.Splynx_ID }}" <a href="https://billing.interphone.com.au/admin/customers/view?id={{ plan.Splynx_ID }}"
target="_blank" class="tag is-info">{{ plan.Splynx_ID }}</a> target="_blank" class="badge bg-info">{{ plan.Splynx_ID }}</a>
</td> </td>
<td> <td>
<strong>{{ plan.Amount | currency }}</strong> <strong>{{ plan.Amount | currency }}</strong>
</td> </td>
<td> <td>
<span class="tag {% if plan.Frequency == 'Weekly' %}is-warning{% elif plan.Frequency == 'Fortnightly' %}is-info{% else %}is-light{% endif %}"> <span class="badge {% if plan.Frequency == 'Weekly' %}bg-warning{% elif plan.Frequency == 'Fortnightly' %}bg-info{% else %}bg-secondary{% endif %}">
{{ plan.Frequency }} {{ plan.Frequency }}
</span> </span>
</td> </td>
<td>{{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }}</td> <td>{{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }}</td>
<td> <td>
{% if plan.Enabled %} {% if plan.Enabled %}
<span class="tag is-success">Active</span> <span class="badge bg-success">Active</span>
{% else %} {% else %}
<span class="tag is-danger">Inactive</span> <span class="badge bg-danger">Inactive</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ plan.Created.strftime('%Y-%m-%d %H:%M') if plan.Created else '-' }}</td> <td>{{ plan.Created.strftime('%Y-%m-%d %H:%M') if plan.Created else '-' }}</td>
<td>{{ plan.created_by or 'Unknown' }}</td> <td>{{ plan.created_by or 'Unknown' }}</td>
<td> <td>
<div class="field is-grouped"> <div class="btn-group btn-group-sm" role="group">
<div class="control"> <a class="btn btn-info btn-sm"
<a class="button is-small is-info"
href="{{ url_for('main.payment_plans_detail', plan_id=plan.id) }}"> 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> </a>
</div> <a class="btn btn-warning btn-sm"
<div class="control">
<a class="button is-small is-warning"
href="{{ url_for('main.payment_plans_edit', plan_id=plan.id) }}"> 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> </a>
</div> </div>
</div> </td>
</td> </tr>
</tr> {% endfor %}
{% endfor %} </tbody>
</tbody> </table>
</table> </div>
</div> {% else %}
{% else %} <div class="text-center py-5">
<div class="has-text-centered py-6"> <i class="fas fa-calendar-alt fa-3x text-muted mb-3"></i>
<span class="icon is-large has-text-grey-light"> <p class="h5 text-muted">No Payment Plans Found</p>
<i class="fas fa-calendar-alt fa-3x"></i> <p class="text-muted">Get started by creating your first payment plan.</p>
</span> <a class="btn btn-primary" href="{{ url_for('main.payment_plans_create') }}">
<p class="title is-5 has-text-grey">No Payment Plans Found</p> <i class="fas fa-plus"></i> Create Payment Plan
<p class="subtitle is-6 has-text-grey">Get started by creating your first payment plan.</p> </a>
<a class="button is-primary" href="{{ url_for('main.payment_plans_create') }}"> </div>
<span class="icon"><i class="fas fa-plus"></i></span> {% endif %}
<span>Create Payment Plan</span>
</a>
</div> </div>
{% endif %}
</div> </div>
<script> <script>
@ -205,12 +257,12 @@ document.addEventListener('DOMContentLoaded', function() {
const row = element.closest('tr'); const row = element.closest('tr');
row.dataset.customerName = data.name.toLowerCase(); row.dataset.customerName = data.name.toLowerCase();
} else { } else {
element.innerHTML = '<span class="has-text-danger">Unknown Customer</span>'; element.innerHTML = '<span class="text-danger">Unknown Customer</span>';
} }
}) })
.catch(error => { .catch(error => {
console.error('Error fetching customer:', 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 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 %} {% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs"> <nav aria-label="breadcrumb">
<ul> <ol class="breadcrumb">
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li> <li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">Single Payment</a></li> <li class="breadcrumb-item active" aria-current="page">Single Payment</li>
</ul> </ol>
</nav> </nav>
<div class="level"> <div class="d-flex justify-content-between align-items-center mb-4">
<div class="level-left"> <div>
<div> <h1 class="h2">Single Payment Processing</h1>
<h1 class="title">Single Payment Processing</h1> <p class="text-muted">Process individual customer payments through Stripe</p>
<p class="subtitle">Process individual customer payments through Stripe</p>
</div>
</div> </div>
</div> </div>
<!-- Single Payment Form --> <!-- Single Payment Form -->
<div class="box"> <div class="card">
<div class="card-body">
<!-- Step 1: Enter Splynx ID --> <!-- Step 1: Enter Splynx ID -->
<div id="step1" class="payment-step"> <div id="step1" class="payment-step">
<h2 class="title is-4"> <h2 class="h4">
<span class="icon"><i class="fas fa-search"></i></span> <i class="fas fa-search me-2"></i>
Customer Lookup Customer Lookup
</h2> </h2>
@ -37,14 +88,14 @@
</div> </div>
<!-- Loading State --> <!-- 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> <div class="spinner"></div>
<p class="mt-3">Fetching customer details...</p> <p class="mt-3">Fetching customer details...</p>
</div> </div>
<!-- Error State --> <!-- Error State -->
<div id="customerError" class="notification is-danger is-hidden"> <div id="customerError" class="alert alert-danger d-none">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span> <i class="fas fa-exclamation-triangle me-2"></i>
<span id="errorMessage">Customer not found or error occurred</span> <span id="errorMessage">Customer not found or error occurred</span>
</div> </div>
@ -58,19 +109,86 @@
</div> </div>
</div> </div>
<!-- Step 2: Confirm Customer & Enter Amount --> <!-- Step 2: Select Invoices (NEW) -->
<div id="step2" class="payment-step is-hidden"> <div id="step2" class="payment-step d-none">
<h2 class="title is-4"> <h2 class="h4">
<span class="icon"><i class="fas fa-user-check"></i></span> <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 Confirm Customer & Payment Details
</h2> </h2>
<div class="box has-background-light mb-5"> <div class="card bg-light mb-5">
<h3 class="subtitle is-5">Customer Information</h3> <div class="card-body">
<h3 class="h5">Customer Information</h3>
<div id="customerDetails"> <div id="customerDetails">
<!-- Customer details will be populated here --> <!-- Customer details will be populated here -->
</div> </div>
</div> </div>
</div> <!-- /.card -->
<form id="paymentForm"> <form id="paymentForm">
<input type="hidden" id="confirmed_splynx_id" name="splynx_id"> <input type="hidden" id="confirmed_splynx_id" name="splynx_id">
@ -99,22 +217,22 @@
<i class="fas fa-credit-card"></i> <i class="fas fa-credit-card"></i>
</span> </span>
</div> </div>
<div id="payment_method_error" class="notification is-danger is-light is-hidden mt-2"> <div id="payment_method_error" class="alert alert-danger d-none mt-2">
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span> <i class="fas fa-exclamation-triangle me-2"></i>
<span>Unable to load payment methods. Customer may not have any valid payment methods.</span> <span>Unable to load payment methods. Customer may not have any valid payment methods.</span>
</div> </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>
<div class="notification is-info is-light"> <div class="alert alert-info">
<span class="icon"><i class="fas fa-info-circle"></i></span> <i class="fas fa-info-circle me-2"></i>
This payment will be processed immediately using the selected payment method. This payment will be processed immediately using the selected payment method.
</div> </div>
</form> </form>
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <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 class="icon"><i class="fas fa-arrow-left"></i></span>
<span>Back</span> <span>Back</span>
</button> </button>
@ -128,144 +246,113 @@
</div> </div>
</div> </div>
</div> </div>
</div> <!-- /.card -->
<!-- Payment Confirmation Modal --> <!-- Payment Confirmation Modal -->
<div class="modal" id="confirmationModal"> <div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true">
<div class="modal-background" onclick="hideModal('confirmationModal')"></div> <div class="modal-dialog modal-dialog-centered">
<div class="modal-card"> <div class="modal-content">
<header class="modal-card-head has-background-warning"> <div class="modal-header bg-warning">
<p class="modal-card-title"> <h5 class="modal-title" id="confirmationModalLabel">Confirm Payment Processing</h5>
<span class="icon"><i class="fas fa-exclamation-triangle"></i></span> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
Confirm Payment Processing </div>
</p> <div class="modal-body">
<button class="delete" aria-label="close" onclick="hideModal('confirmationModal')"></button> <p><strong>Customer:</strong> <span id="confirmCustomerName"></span></p>
</header> <p><strong>Amount:</strong> <span id="confirmAmount"></span></p>
<section class="modal-card-body"> <p><strong>Payment Method:</strong> <span id="confirmPaymentMethod"></span></p>
<div class="content"> <p class="mb-0">Are you sure you want to process this payment?</p>
<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> </div>
</section> <div class="modal-footer">
<footer class="modal-card-foot"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button class="button is-danger" id="confirmPaymentBtn" onclick="processPayment()"> <button type="button" class="btn btn-warning" id="confirmPaymentBtn" onclick="processPayment()">
<span class="icon"><i class="fas fa-credit-card"></i></span> <i class="fas fa-credit-card me-2"></i>Confirm Payment
<span>Confirm & Process Payment</span> </button>
</button> </div>
<button class="button" onclick="hideModal('confirmationModal')">Cancel</button> </div>
</footer>
</div> </div>
</div> </div>
<!-- Success Modal --> <!-- Success Modal -->
<div class="modal" id="successModal"> <div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-background"></div> <div class="modal-dialog modal-dialog-centered">
<div class="modal-card"> <div class="modal-content">
<header class="modal-card-head has-background-success"> <div class="modal-header bg-success text-white">
<p class="modal-card-title has-text-white"> <h5 class="modal-title" id="successModalLabel">
<span class="icon"><i class="fas fa-check-circle"></i></span> <i class="fas fa-check-circle me-2"></i>Payment Successful
Payment Successful </h5>
</p> </div>
</header> <div class="modal-body">
<section class="modal-card-body"> <div class="text-center py-4">
<div class="has-text-centered py-4"> <i class="fas fa-check-circle fa-3x text-success mb-4"></i>
<span class="icon is-large has-text-success mb-4"> <h3 class="h4">Payment Processed Successfully!</h3>
<i class="fas fa-check-circle fa-3x"></i> <div id="successMessage" class="mt-3">
</span> <!-- Success details will be populated here -->
<h3 class="title is-4">Payment Processed Successfully!</h3> </div>
<div id="successMessage" class="content">
<!-- Success details will be populated here -->
</div> </div>
</div> </div>
</section> <div class="modal-footer justify-content-center">
<footer class="modal-card-foot is-justify-content-center"> <button type="button" class="btn btn-primary" onclick="closeSuccessModal()">
<button class="button is-primary" onclick="closeSuccessModal()"> <i class="fas fa-check me-2"></i>Close
<span class="icon"><i class="fas fa-check"></i></span> </button>
<span>Close</span> </div>
</button> </div>
</footer>
</div> </div>
</div> </div>
<!-- Fee Update Modal (Orange) --> <!-- Fee Update Modal (Orange) -->
<div class="modal" id="feeUpdateModal"> <div class="modal fade" id="feeUpdateModal" tabindex="-1" aria-labelledby="feeUpdateModalLabel" aria-hidden="true">
<div class="modal-background"></div> <div class="modal-dialog modal-dialog-centered">
<div class="modal-card"> <div class="modal-content">
<header class="modal-card-head has-background-warning"> <div class="modal-header bg-warning">
<p class="modal-card-title has-text-dark"> <h5 class="modal-title" id="feeUpdateModalLabel">
<span class="icon"><i class="fas fa-clock"></i></span> <i class="fas fa-clock me-2"></i>Direct Debit Processing
Direct Debit Processing </h5>
</p> </div>
</header> <div class="modal-body">
<section class="modal-card-body"> <div class="text-center py-4">
<div class="has-text-centered py-4"> <i class="fas fa-clock fa-3x text-warning mb-4"></i>
<span class="icon is-large has-text-warning mb-4"> <h3 class="h4">Direct Debit is still being processed</h3>
<i class="fas fa-clock fa-3x"></i> <div class="mt-3">
</span> <p>Your Direct Debit payment is currently being processed by the bank. This can take a few minutes to complete.</p>
<h3 class="title is-4">Direct Debit is still being processed</h3> <p><strong>Please check back later or click the button below to view payment details.</strong></p>
<div class="content"> </div>
<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> </div>
</section> <div class="modal-footer justify-content-center">
<footer class="modal-card-foot is-justify-content-center"> <button type="button" class="btn btn-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()">
<button class="button is-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()"> <i class="fas fa-eye me-2"></i>View Payment Details
<span class="icon"><i class="fas fa-eye"></i></span> </button>
<span>View Payment Details</span> </div>
</button> </div>
</footer>
</div> </div>
</div> </div>
<!-- Error Modal --> <!-- Error Modal -->
<div class="modal" id="errorModal"> <div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-background" onclick="hideModal('errorModal')"></div> <div class="modal-dialog modal-dialog-centered">
<div class="modal-card"> <div class="modal-content">
<header class="modal-card-head has-background-danger"> <div class="modal-header bg-danger text-white">
<p class="modal-card-title has-text-white"> <h5 class="modal-title" id="errorModalLabel">
<span class="icon"><i class="fas fa-exclamation-circle"></i></span> <i class="fas fa-exclamation-circle me-2"></i>Payment Failed
Payment Failed </h5>
</p> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
<button class="delete" aria-label="close" onclick="hideModal('errorModal')"></button> </div>
</header> <div class="modal-body">
<section class="modal-card-body"> <div class="text-center py-4">
<div class="has-text-centered py-4"> <i class="fas fa-exclamation-circle fa-3x text-danger mb-4"></i>
<span class="icon is-large has-text-danger mb-4"> <h3 class="h4">Payment Processing Failed</h3>
<i class="fas fa-exclamation-circle fa-3x"></i> <div id="errorDetails" class="mt-3">
</span> <!-- Error details will be populated here -->
<h3 class="title is-4">Payment Processing Failed</h3> </div>
<div id="errorDetails" class="content">
<!-- Error details will be populated here -->
</div> </div>
</div> </div>
</section> <div class="modal-footer justify-content-center">
<footer class="modal-card-foot is-justify-content-center"> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">
<button class="button is-danger" onclick="hideModal('errorModal')"> <i class="fas fa-times me-2"></i>Close
<span class="icon"><i class="fas fa-times"></i></span> </button>
<span>Close</span> </div>
</button> </div>
</footer>
</div> </div>
</div> </div>
@ -290,7 +377,7 @@
transition: opacity 0.3s ease, transform 0.3s ease; transition: opacity 0.3s ease, transform 0.3s ease;
} }
.payment-step.is-hidden { .payment-step.d-none {
display: none; display: none;
} }
@ -299,31 +386,20 @@
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; 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> </style>
<script> <script>
let currentCustomerData = null; let currentCustomerData = null;
let currentPaymentId = null; let currentPaymentId = null;
let allInvoices = [];
let selectedInvoices = [];
function fetchCustomerDetails() { function fetchCustomerDetails() {
const splynxIdElement = document.getElementById('lookup_splynx_id'); const splynxIdElement = document.getElementById('lookup_splynx_id');
const splynxId = splynxIdElement ? splynxIdElement.value : ''; const splynxId = splynxIdElement ? splynxIdElement.value : '';
// Clear previous errors // Clear previous errors
document.getElementById('customerError').classList.add('is-hidden'); document.getElementById('customerError').classList.add('d-none');
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') { if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
showError('Please enter a valid Splynx Customer ID'); showError('Please enter a valid Splynx Customer ID');
@ -331,7 +407,7 @@ function fetchCustomerDetails() {
} }
// Show loading state // Show loading state
document.getElementById('loading').classList.remove('is-hidden'); document.getElementById('loading').classList.remove('d-none');
document.getElementById('nextBtn').disabled = true; document.getElementById('nextBtn').disabled = true;
const apiUrl = `/api/splynx/${splynxId.trim()}`; const apiUrl = `/api/splynx/${splynxId.trim()}`;
@ -346,7 +422,7 @@ function fetchCustomerDetails() {
}) })
.then(data => { .then(data => {
// Hide loading // Hide loading
document.getElementById('loading').classList.add('is-hidden'); document.getElementById('loading').classList.add('d-none');
document.getElementById('nextBtn').disabled = false; document.getElementById('nextBtn').disabled = false;
if (data && data.id) { if (data && data.id) {
@ -359,7 +435,7 @@ function fetchCustomerDetails() {
}) })
.catch(error => { .catch(error => {
console.error('Error fetching customer:', 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; document.getElementById('nextBtn').disabled = false;
showError(`Failed to fetch customer details: ${error.message}`); showError(`Failed to fetch customer details: ${error.message}`);
}); });
@ -367,32 +443,32 @@ function fetchCustomerDetails() {
function displayCustomerDetails(customer) { function displayCustomerDetails(customer) {
const detailsHtml = ` const detailsHtml = `
<div class="columns is-multiline"> <div class="row g-3">
<div class="column is-half"> <div class="col-md-6">
<strong>Name:</strong><br> <strong>Name:</strong><br>
<span>${customer.name || 'N/A'}</span> <span>${customer.name || 'N/A'}</span>
</div> </div>
<div class="column is-half"> <div class="col-md-6">
<strong>Customer ID:</strong><br> <strong>Customer ID:</strong><br>
<span class="tag is-info">${customer.id}</span> <span class="badge bg-info">${customer.id}</span>
</div> </div>
<div class="column is-half"> <div class="col-md-6">
<strong>Status:</strong><br> <strong>Status:</strong><br>
${customer.status === 'active' ${customer.status === 'active'
? '<span class="tag is-success">Active</span>' ? '<span class="badge bg-success">Active</span>'
: `<span class="tag is-warning">${customer.status || 'Unknown'}</span>` : `<span class="badge bg-warning">${customer.status || 'Unknown'}</span>`
} }
</div> </div>
<div class="column is-half"> <div class="col-md-6">
<strong>Email:</strong><br> <strong>Email:</strong><br>
<span>${customer.email || 'N/A'}</span> <span>${customer.email || 'N/A'}</span>
</div> </div>
<div class="column is-full"> <div class="col-12">
<strong>Address:</strong><br> <strong>Address:</strong><br>
<span>${customer.street_1 || ''} ${customer.street_2 || ''}<br> <span>${customer.street_1 || ''} ${customer.street_2 || ''}<br>
${customer.city || ''} ${customer.zip_code || ''}</span> ${customer.city || ''} ${customer.zip_code || ''}</span>
</div> </div>
<div class="column is-half"> <div class="col-md-6">
<strong>Phone:</strong><br> <strong>Phone:</strong><br>
<span>${customer.phone || 'N/A'}</span> <span>${customer.phone || 'N/A'}</span>
</div> </div>
@ -404,11 +480,14 @@ function displayCustomerDetails(customer) {
// Fetch payment methods for this customer // Fetch payment methods for this customer
fetchPaymentMethods(customer.id); fetchPaymentMethods(customer.id);
// Fetch invoices for this customer
fetchInvoices(customer.id);
} }
function showError(message) { function showError(message) {
document.getElementById('errorMessage').textContent = message; document.getElementById('errorMessage').textContent = message;
document.getElementById('customerError').classList.remove('is-hidden'); document.getElementById('customerError').classList.remove('d-none');
} }
function fetchPaymentMethods(splynxId) { function fetchPaymentMethods(splynxId) {
@ -475,7 +554,7 @@ function displayPaymentMethods(paymentMethods) {
`; `;
// Hide any error messages // Hide any error messages
document.getElementById('payment_method_error').classList.add('is-hidden'); document.getElementById('payment_method_error').classList.add('d-none');
} }
function showPaymentMethodError() { function showPaymentMethodError() {
@ -494,29 +573,23 @@ function showPaymentMethodError() {
`; `;
// Show error notification // Show error notification
document.getElementById('payment_method_error').classList.remove('is-hidden'); document.getElementById('payment_method_error').classList.remove('d-none');
} }
function goToStep2() { function goToStep2() {
// Hide step 1, show step 2 // Hide step 1, show step 2 (invoice selection)
document.getElementById('step1').classList.add('is-hidden'); document.getElementById('step1').classList.add('d-none');
document.getElementById('step2').classList.remove('is-hidden'); document.getElementById('step2').classList.remove('d-none');
// Focus on amount input
document.getElementById('payment_amount').focus();
} }
function goBackToStep1() { function goBackToStep1() {
// Show step 1, hide step 2 // Show step 1, hide step 2
document.getElementById('step1').classList.remove('is-hidden'); document.getElementById('step1').classList.remove('d-none');
document.getElementById('step2').classList.add('is-hidden'); document.getElementById('step2').classList.add('d-none');
// Clear any errors // Clear any errors
document.getElementById('customerError').classList.add('is-hidden'); document.getElementById('customerError').classList.add('d-none');
document.getElementById('payment_method_error').classList.add('is-hidden'); document.getElementById('payment_method_error').classList.add('d-none');
// Clear form
document.getElementById('payment_amount').value = '';
// Reset payment method selector to loading state // Reset payment method selector to loading state
const container = document.getElementById('payment_method_container'); 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() { function showConfirmationModal() {
const amount = document.getElementById('payment_amount').value; const amount = document.getElementById('payment_amount').value;
const paymentMethodSelect = document.getElementById('payment_method_select'); const paymentMethodSelect = document.getElementById('payment_method_select');
@ -556,14 +718,19 @@ function showConfirmationModal() {
document.getElementById('confirmAmount').textContent = `$${parseFloat(amount).toFixed(2)}`; document.getElementById('confirmAmount').textContent = `$${parseFloat(amount).toFixed(2)}`;
document.getElementById('confirmPaymentMethod').textContent = paymentMethodSelect.options[paymentMethodSelect.selectedIndex].text; document.getElementById('confirmPaymentMethod').textContent = paymentMethodSelect.options[paymentMethodSelect.selectedIndex].text;
// Show modal // Show modal using Bootstrap Modal API
document.getElementById('confirmationModal').classList.add('is-active'); const modalEl = document.getElementById('confirmationModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
} }
function processPayment() { function processPayment() {
const form = document.getElementById('paymentForm'); const form = document.getElementById('paymentForm');
const formData = new FormData(form); const formData = new FormData(form);
// Add selected invoice IDs
formData.append('invoice_ids', selectedInvoices.join(','));
// Disable confirm button and show loading // Disable confirm button and show loading
const confirmBtn = document.getElementById('confirmPaymentBtn'); const confirmBtn = document.getElementById('confirmPaymentBtn');
const originalText = confirmBtn.innerHTML; const originalText = confirmBtn.innerHTML;
@ -631,21 +798,36 @@ function showSuccessModal(data) {
`; `;
document.getElementById('successMessage').innerHTML = successHtml; 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) { function showErrorModal(errorMessage) {
document.getElementById('errorDetails').innerHTML = `<p>${errorMessage}</p>`; 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) { 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) { function showFeeUpdateModal(data) {
currentPaymentId = data.payment_id; 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() { function viewPaymentDetails() {
@ -660,16 +842,13 @@ function closeSuccessModal() {
// Reset form to step 1 // Reset form to step 1
goBackToStep1(); goBackToStep1();
document.getElementById('lookup_splynx_id').value = ''; document.getElementById('lookup_splynx_id').value = '';
document.getElementById('payment_amount').value = '';
currentCustomerData = null; currentCustomerData = null;
allInvoices = [];
selectedInvoices = [];
} }
// Close modals on escape key // Bootstrap modals handle Escape key automatically, no need for custom handler
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const activeModals = document.querySelectorAll('.modal.is-active');
activeModals.forEach(modal => modal.classList.remove('is-active'));
}
});
// Enter key navigation // Enter key navigation
document.getElementById('lookup_splynx_id').addEventListener('keypress', function(event) { 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 %} {% block head %}
<style> <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 { .search-container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
@ -20,15 +77,15 @@
.search-input { .search-input {
font-size: 1.2rem; font-size: 1.2rem;
padding: 1rem; padding: 1rem;
border: 2px solid #dbdbdb; border: 2px solid #dee2e6;
border-radius: 6px; border-radius: 6px;
transition: border-color 0.3s; transition: border-color 0.3s;
} }
.search-input:focus { .search-input:focus {
border-color: #3273dc; border-color: #0d6efd;
outline: none; 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 { .search-results {
@ -53,7 +110,7 @@
.result-header { .result-header {
display: flex; display: flex;
justify-content: between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -73,7 +130,7 @@
} }
.result-type.batch { .result-type.batch {
background-color: #3273dc; background-color: #0d6efd;
color: white; color: white;
} }
@ -136,61 +193,49 @@
{% block content %} {% block content %}
<div class="search-container"> <div class="search-container">
<div class="search-box"> <div class="search-box">
<h1 class="title is-2">🔍 Payment Search</h1> <h1 class="h2 mb-2">🔍 Payment Search</h1>
<p class="subtitle">Search across all payment records by Splynx ID or Payment Intent</p> <p class="text-muted mb-4">Search across all payment records by Splynx ID or Payment Intent</p>
<div class="field has-addons"> <div class="input-group input-group-lg mb-3">
<div class="control is-expanded"> <input
<input class="form-control search-input"
class="input search-input" type="text"
type="text" id="searchQuery"
id="searchQuery" placeholder="Enter Splynx ID (e.g. 123456) or Payment Intent (e.g. pi_1234567890)"
placeholder="Enter Splynx ID (e.g. 123456) or Payment Intent (e.g. pi_1234567890)" autocomplete="off"
autocomplete="off" >
> <button class="btn btn-primary" type="button" onclick="performSearch()">
</div> <i class="fas fa-search"></i> Search
<div class="control"> </button>
<button class="button is-primary is-large" onclick="performSearch()">
<span class="icon">
<i class="fas fa-search"></i>
</span>
<span>Search</span>
</button>
</div>
</div> </div>
<div class="field is-grouped is-grouped-multiline" style="margin-top: 1rem;"> <div class="row g-3 mb-3">
<div class="control"> <div class="col-md-4">
<label class="label is-small">Search Type:</label> <label class="form-label form-label-sm">Search Type:</label>
<div class="select is-small"> <select class="form-select form-select-sm" id="searchType">
<select id="searchType"> <option value="all">Auto-detect</option>
<option value="all">Auto-detect</option> <option value="splynx_id">Splynx ID</option>
<option value="splynx_id">Splynx ID</option> <option value="payment_intent">Payment Intent</option>
<option value="payment_intent">Payment Intent</option> </select>
</select>
</div>
</div> </div>
<div class="control"> <div class="col-md-4">
<label class="label is-small">Results Limit:</label> <label class="form-label form-label-sm">Results Limit:</label>
<div class="select is-small"> <select class="form-select form-select-sm" id="resultsLimit">
<select id="resultsLimit"> <option value="25">25 results</option>
<option value="25">25 results</option> <option value="50" selected>50 results</option>
<option value="50" selected>50 results</option> <option value="100">100 results</option>
<option value="100">100 results</option> </select>
</select>
</div>
</div> </div>
<div class="control"> <div class="col-md-4 d-flex align-items-end">
<button class="button is-small is-info is-outlined" onclick="clearSearch()"> <button class="btn btn-sm btn-outline-info" onclick="clearSearch()">
<span class="icon"><i class="fas fa-times"></i></span> <i class="fas fa-times"></i> Clear
<span>Clear</span>
</button> </button>
</div> </div>
</div> </div>
<div class="search-tips"> <div class="search-tips">
<h5 class="title is-6">💡 Search Tips:</h5> <h5 class="h6 mb-2">💡 Search Tips:</h5>
<ul> <ul class="mb-0">
<li><strong>Splynx ID:</strong> Enter customer ID number (e.g., 123456)</li> <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>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> <li><strong>Auto-detect:</strong> System automatically detects search type based on format</li>
@ -261,8 +306,10 @@ function showLoading() {
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="search-results"> <div class="search-results">
<div class="loading"> <div class="loading">
<div class="loader"></div> <div class="spinner-border text-primary" role="status">
<p>Searching payments...</p> <span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Searching payments...</p>
</div> </div>
</div> </div>
`; `;
@ -272,7 +319,7 @@ function showError(message) {
const resultsDiv = document.getElementById('searchResults'); const resultsDiv = document.getElementById('searchResults');
resultsDiv.style.display = 'block'; resultsDiv.style.display = 'block';
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="search-error"> <div class="alert alert-danger" role="alert">
<strong>Search Error:</strong> ${message} <strong>Search Error:</strong> ${message}
</div> </div>
`; `;
@ -285,9 +332,9 @@ function displayResults(data) {
resultsDiv.innerHTML = ` resultsDiv.innerHTML = `
<div class="search-results"> <div class="search-results">
<div class="no-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>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>
</div> </div>
`; `;
@ -296,9 +343,9 @@ function displayResults(data) {
let resultsHtml = ` let resultsHtml = `
<div class="search-results"> <div class="search-results">
<div style="padding: 1rem; border-bottom: 1px solid #f5f5f5; background-color: #f9f9f9;"> <div class="p-3 border-bottom bg-light">
<h3 class="title is-4">Search Results</h3> <h3 class="h4 mb-2">Search Results</h3>
<p>Found ${data.total_found} payment(s) for "${data.search_query}" (${data.search_type})</p> <p class="mb-0">Found ${data.total_found} payment(s) for "${data.search_query}" (${data.search_type})</p>
</div> </div>
`; `;
@ -311,9 +358,9 @@ function displayResults(data) {
} }
function createResultItem(result) { function createResultItem(result) {
const statusClass = result.success === true ? 'has-background-success-light' : const statusClass = result.success === true ? 'bg-success bg-opacity-10' :
result.success === false ? 'has-background-danger-light' : result.success === false ? 'bg-danger bg-opacity-10' :
'has-background-warning-light'; 'bg-warning bg-opacity-10';
const statusText = result.success === true ? 'Success' : const statusText = result.success === true ? 'Success' :
result.success === false ? 'Failed' : result.success === false ? 'Failed' :
@ -328,16 +375,16 @@ function createResultItem(result) {
<div class="result-header"> <div class="result-header">
<div> <div>
<span class="result-type ${result.type}">${result.type}</span> <span class="result-type ${result.type}">${result.type}</span>
<strong style="margin-left: 0.5rem;">Payment #${result.id}</strong> <strong class="ms-2">Payment #${result.id}</strong>
${result.batch_id ? `<span class="tag is-info is-light">Batch #${result.batch_id}</span>` : ''} ${result.batch_id ? `<span class="badge bg-info ms-2">Batch #${result.batch_id}</span>` : ''}
</div> </div>
<div class="tags"> <div>
<span class="tag ${result.success === true ? 'is-success' : result.success === false ? 'is-danger' : 'is-warning'}"> <span class="badge ${result.success === true ? 'bg-success' : result.success === false ? 'bg-danger' : 'bg-warning'}">
<i class="fas ${statusIcon}"></i>&nbsp;${statusText} <i class="fas ${statusIcon}"></i> ${statusText}
</span> </span>
${result.refund ? '<span class="tag is-purple">Refunded</span>' : ''} ${result.refund ? '<span class="badge bg-purple ms-1">Refunded</span>' : ''}
${result.pi_followup ? '<span class="tag is-warning">PI Follow-up</span>' : ''} ${result.pi_followup ? '<span class="badge bg-warning ms-1">PI Follow-up</span>' : ''}
${result.refund_followup ? '<span class="tag is-info">Refund Follow-up</span>' : ''} ${result.refund_followup ? '<span class="badge bg-info ms-1">Refund Follow-up</span>' : ''}
</div> </div>
</div> </div>
@ -354,7 +401,7 @@ function createResultItem(result) {
</div> </div>
<div class="result-field"> <div class="result-field">
<strong>Payment Intent:</strong> <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>
<div class="result-field"> <div class="result-field">
<strong>Created:</strong> ${new Date(result.created).toLocaleDateString()} ${new Date(result.created).toLocaleTimeString()} <strong>Created:</strong> ${new Date(result.created).toLocaleDateString()} ${new Date(result.created).toLocaleTimeString()}
@ -365,26 +412,23 @@ function createResultItem(result) {
</div> </div>
${result.error ? ` ${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 ? '...' : ''} <strong>Error:</strong> ${result.error.substring(0, 200)}${result.error.length > 200 ? '...' : ''}
</div> </div>
` : ''} ` : ''}
<div class="result-actions"> <div class="result-actions">
<a href="${result.detail_url}" class="button is-small is-primary"> <a href="${result.detail_url}" class="btn btn-sm btn-primary">
<span class="icon"><i class="fas fa-eye"></i></span> <i class="fas fa-eye"></i> View Details
<span>View Details</span>
</a> </a>
${result.batch_url ? ` ${result.batch_url ? `
<a href="${result.batch_url}" class="button is-small is-info"> <a href="${result.batch_url}" class="btn btn-sm btn-info">
<span class="icon"><i class="fas fa-layer-group"></i></span> <i class="fas fa-layer-group"></i> View Batch
<span>View Batch</span>
</a> </a>
` : ''} ` : ''}
${result.splynx_url ? ` ${result.splynx_url ? `
<a href="${result.splynx_url}" target="_blank" class="button is-small is-link"> <a href="${result.splynx_url}" target="_blank" class="btn btn-sm btn-link">
<span class="icon"><i class="fas fa-external-link-alt"></i></span> <i class="fas fa-external-link-alt"></i> Splynx Customer
<span>Splynx Customer</span>
</a> </a>
` : ''} ` : ''}
</div> </div>
@ -400,26 +444,11 @@ function clearSearch() {
document.getElementById('searchQuery').focus(); document.getElementById('searchQuery').focus();
} }
// CSS for loader // Purple badge custom style
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
.loader { .bg-purple {
border: 3px solid #f3f3f3; background-color: #9370db !important;
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;
color: white; color: white;
} }
`; `;

Loading…
Cancel
Save