From 6357c84a6262c1fc306e851126d4189df2093b05 Mon Sep 17 00:00:00 2001 From: Alan Woodman Date: Mon, 19 Jan 2026 11:25:29 +0800 Subject: [PATCH] Rewrite of everything --- app.py | 22 +- blueprints/main.py | 170 +- config.py | 6 +- models.py | 8 +- payment_processors/batch_processor.py | 2 +- static/css/custom.css | 819 ++++---- templates/analytics/dashboard.html | 767 ++++---- templates/auth/add_user.html | 164 +- templates/auth/list_users.html | 93 +- templates/auth/login.html | 120 +- templates/base.html | 246 +-- templates/base_original.html | 195 ++ templates/hades/base.html | 627 ++++++ templates/hades/location_ont_details.html | 2161 +++++++++++++++++++++ templates/main/add_payment_method.html | 61 + templates/main/batch_detail.html | 1255 +++++++----- templates/main/batch_list.html | 121 +- templates/main/batch_payment_detail.html | 522 +++++ templates/main/index.html | 24 +- templates/main/logs_list.html | 535 ++--- templates/main/payment_detail.html | 1143 +++++------ templates/main/payment_plans_form.html | 61 + templates/main/payment_plans_list.html | 374 ++-- templates/main/single_payment.html | 619 +++--- templates/main/single_payment_detail.html | 1221 +++++++----- templates/main/single_payments_list.html | 1085 +++++++---- templates/search/search.html | 275 +-- 27 files changed, 8711 insertions(+), 3985 deletions(-) create mode 100644 templates/base_original.html create mode 100644 templates/hades/base.html create mode 100644 templates/hades/location_ont_details.html create mode 100644 templates/main/batch_payment_detail.html diff --git a/app.py b/app.py index 0884b4e..efd0756 100644 --- a/app.py +++ b/app.py @@ -4,6 +4,7 @@ from flask_migrate import Migrate from flask_login import LoginManager import pymysql import os +import json from config import Config db = SQLAlchemy() @@ -74,7 +75,7 @@ def create_app(): def inject_permissions(): return { 'can_manage_users': can_manage_users, - 'can_manage_payments': can_manage_payments, + 'can_manage_payments': can_manage_payments, 'can_view_data': can_view_data, 'can_process_single_payments': can_process_single_payments, 'can_manage_batch_payments': can_manage_batch_payments, @@ -84,7 +85,24 @@ def create_app(): 'has_permission': has_permission, 'get_user_permission_level': get_user_permission_level } - + + # Add custom Jinja2 filter for JSON formatting + @app.template_filter('format_json') + def format_json_filter(value): + """Format JSON string with proper indentation.""" + if not value: + return '' + try: + # If it's already a dict/list, just dump it + if isinstance(value, (dict, list)): + return json.dumps(value, indent=2) + # If it's a string, parse and re-dump with formatting + parsed = json.loads(value) + return json.dumps(parsed, indent=2) + except (json.JSONDecodeError, TypeError): + # If it fails to parse, return original value + return value + # Note: Database tables will be managed by Flask-Migrate # Use 'flask db init', 'flask db migrate', 'flask db upgrade' commands diff --git a/blueprints/main.py b/blueprints/main.py index f4f7ffc..c57ef6a 100644 --- a/blueprints/main.py +++ b/blueprints/main.py @@ -1,9 +1,11 @@ -from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for -from flask_login import login_required, current_user -from sqlalchemy import func, case import json import pymysql +from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app +from flask_login import login_required, current_user +from sqlalchemy import func, case +from datetime import datetime from app import db +from typing import Dict, Any, List from models import PaymentBatch, Payments, SinglePayments, PaymentPlans, Logs, Users from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET from stripe_payment_processor import StripePaymentProcessor @@ -307,50 +309,55 @@ def processPaymentResult(pay_id, result, key): print(f"processPaymentResult error: {e}\n{json.dumps(result)}") payment.PI_FollowUp = True -def find_pay_splynx_invoices(splynx_id): +def find_pay_splynx_invoices(splynx_id: int, splynx_pay_id: int, invoice_ids: List[int]) -> List[int]: """Mark Splynx invoices as paid for the given customer ID.""" #result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid") - params = { - 'main_attributes': { - 'customer_id': splynx_id, - 'status': ['IN', ['not_paid', 'pending']] - }, - } - query_string = splynx.build_splynx_query_params(params) - result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") + print(f"\n\nInvoice IDs to Pay: {invoice_ids} of type {type(invoice_ids)}\n") + + #params = { + # 'main_attributes': { + # 'customer_id': splynx_id, + # 'status': ['IN', ['not_paid', 'pending']] + # }, + #} + #query_string = splynx.build_splynx_query_params(params) + #result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") invoice_pay = { - "status": "paid" + "status": "paid", + "payment_id": splynx_pay_id, + "date_payment": datetime.now().strftime("%Y-%m-%d") } - for pay in result: - res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) - return res + #for pay in result: + for invoice in invoice_ids: + res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pay) + return res -def find_set_pending_splynx_invoices(splynx_id): +def find_set_pending_splynx_invoices(splynx_id: int, invoice_list: List): """Mark Splynx invoices as pending for the given customer ID.""" - params = { - 'main_attributes': { - 'customer_id': splynx_id, - 'status': 'not_paid' - }, - } - query_string = splynx.build_splynx_query_params(params) - result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") + #params = { + # 'main_attributes': { + # 'customer_id': splynx_id, + # 'status': 'not_paid' + # }, + #} + #query_string = splynx.build_splynx_query_params(params) + #result = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") invoice_pending = { "status": "pending" } updated_invoices = [] - for invoice in result: - res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice['id']}", params=invoice_pending) + for invoice in invoice_list: + res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{invoice}", params=invoice_pending) if res: updated_invoices.append(res) return updated_invoices -def add_payment_splynx(splynx_id, pi_id, pay_id, amount): +def add_payment_splynx(splynx_id, pi_id, pay_id, amount, invoice_id): """Add a payment record to Splynx.""" from datetime import datetime @@ -359,7 +366,8 @@ def add_payment_splynx(splynx_id, pi_id, pay_id, amount): "amount": amount, "date": str(datetime.now().strftime('%Y-%m-%d')), "field_1": pi_id, - "field_2": f"Single Payment_ID: {pay_id}" + "field_2": f"Single Payment_ID: {pay_id}", + "invoice_id": invoice_id } res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay) @@ -749,11 +757,12 @@ def payment_detail(payment_id): """Display detailed view of a specific batch payment.""" # Get payment information with all fields needed for the detail view payment = db.session.query(Payments).filter(Payments.id == payment_id).first() - + if not payment: - flash('Payment not found.', 'error') + current_app.logger.warning(f"Payment ID {payment_id} not found in Payments table") + flash(f'Payment #{payment_id} not found in batch payments.', 'error') return redirect(url_for('main.batch_list')) - + # Log the payment detail view access log_activity( user_id=current_user.id, @@ -762,8 +771,13 @@ def payment_detail(payment_id): entity_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/', methods=['POST']) @login_required @@ -814,6 +828,37 @@ def check_payment_intent(payment_id): print(f"Check payment intent error: {e}") return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500 +@main_bp.route('/api/splynx/invoices/', methods=['GET']) +@helpdesk_required +def get_customer_invoices(splynx_id): + """Fetch unpaid invoices for a customer from Splynx.""" + try: + params = { + 'main_attributes': { + 'customer_id': splynx_id, + 'status': 'not_paid' + } + } + query_string = splynx.build_splynx_query_params(params) + invoices = splynx.get(url=f"/api/2.0/admin/finance/invoices?{query_string}") + + # Format invoice data for frontend + formatted_invoices = [] + for inv in invoices: + formatted_invoices.append({ + 'id': inv['id'], + 'number': inv.get('number', 'N/A'), + 'date': inv.get('date', 'N/A'), + 'total': float(inv.get('total', 0)), + 'status': inv.get('status', 'unknown'), + 'description': inv.get('memo', 'No description') + }) + + return jsonify({'success': True, 'invoices': formatted_invoices}) + except Exception as e: + print(f"Error fetching invoices: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + @main_bp.route('/single-payment/process', methods=['POST']) @helpdesk_required def process_single_payment(): @@ -823,6 +868,8 @@ def process_single_payment(): splynx_id = request.form.get('splynx_id') amount = request.form.get('amount') payment_method = request.form.get('payment_method') + invoice_ids = request.form.get('invoice_ids', []) + invoice_list = invoice_ids.split(",") if invoice_ids else [0] # Validate inputs if not splynx_id or not amount or not payment_method: @@ -852,7 +899,8 @@ def process_single_payment(): Splynx_ID=splynx_id, Stripe_Customer_ID=stripe_customer_id, Payment_Amount=amount, - Who=current_user.id + Who=current_user.id, + Invoices_to_Pay=invoice_ids ) db.session.add(payment_record) db.session.commit() # Commit to get the payment ID @@ -982,18 +1030,20 @@ def process_single_payment(): if result.get('needs_fee_update'): payment_record.PI_FollowUp = True # Mark invoices as pending when PI_FollowUp is set - if Config.PROCESS_LIVE: - try: - find_set_pending_splynx_invoices(splynx_id) - except Exception as e: - print(f"⚠️ Error setting invoices to pending: {e}") + #if Config.PROCESS_LIVE: + try: + pending_invoices = find_set_pending_splynx_invoices(splynx_id, invoice_list) + if invoice_list[0] == 0: + payment_record.Invoices_to_Pay = ','.join(pending_invoices) + except Exception as e: + print(f"⚠️ Error setting invoices to pending: {e}") if result.get('payment_method_type') == "card": payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card') elif result.get('payment_method_type') == "au_becs_debit": payment_record.Payment_Method = result['payment_method_type'] - if result.get('fee_details'): + if result.get('fee_details') and result.get('fee_details').get('fee_breakdown'): payment_record.Fee_Total = result['fee_details']['total_fee'] for fee_type in result['fee_details']['fee_breakdown']: if fee_type['type'] == "tax": @@ -1007,27 +1057,27 @@ def process_single_payment(): # Check if payment was actually successful if result.get('success'): # Payment succeeded - update Splynx if in live mode - if Config.PROCESS_LIVE: - try: + #if Config.PROCESS_LIVE: + try: + # Add payment record to Splynx + splynx_payment_id = add_payment_splynx( + splynx_id=splynx_id, + pi_id=result.get('payment_intent_id'), + pay_id=payment_record.id, + amount=amount, + invoice_id=invoice_list[0] + ) + + if splynx_payment_id: + print(f"✅ Splynx payment record created: {splynx_payment_id}") # Mark invoices as paid in Splynx - find_pay_splynx_invoices(splynx_id) - - # Add payment record to Splynx - splynx_payment_id = add_payment_splynx( - splynx_id=splynx_id, - pi_id=result.get('payment_intent_id'), - pay_id=payment_record.id, - amount=amount - ) + find_pay_splynx_invoices(splynx_id, splynx_payment_id, invoice_list) + else: + print("⚠️ Failed to create Splynx payment record") - if splynx_payment_id: - print(f"✅ Splynx payment record created: {splynx_payment_id}") - else: - print("⚠️ Failed to create Splynx payment record") - - except Exception as splynx_error: - print(f"❌ Error updating Splynx: {splynx_error}") - # Continue processing even if Splynx update fails + except Exception as splynx_error: + print(f"❌ Error updating Splynx: {splynx_error}") + # Continue processing even if Splynx update fails # Log successful payment log_activity( diff --git a/config.py b/config.py index 9876f7a..260a7f7 100644 --- a/config.py +++ b/config.py @@ -5,8 +5,8 @@ class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'plutus-dev-secret-key-change-in-production' # PostgreSQL database configuration (Flask-SQLAlchemy) - #SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus' - SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus' + SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus' + #SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:strong_password@10.0.1.15/plutus' SQLALCHEMY_TRACK_MODIFICATIONS = False @@ -37,7 +37,7 @@ class Config: # Threading configuration MAX_PAYMENT_THREADS = 15 # Number of concurrent payment processing threads - THREAD_TIMEOUT = 60 # Timeout in seconds for payment processing threads + THREAD_TIMEOUT = 30 # Timeout in seconds for payment processing threads # Stripe API Keys STRIPE_LIVE_API_KEY = os.environ.get('STRIPE_LIVE_API_KEY') or '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' diff --git a/models.py b/models.py index aeb890f..0d87555 100644 --- a/models.py +++ b/models.py @@ -30,7 +30,7 @@ class Users(UserMixin, db.Model): class PaymentBatch(db.Model): __tablename__ = 'PaymentBatch' id = db.Column(db.Integer, primary_key=True) - Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) class Payments(db.Model): @@ -58,7 +58,7 @@ class Payments(db.Model): Refund_JSON = db.Column(db.Text()) Stripe_Refund_ID = db.Column(db.String()) Stripe_Refund_Created = db.Column(db.DateTime, nullable=True) - Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) PaymentPlan_ID = db.Column(db.Integer, db.ForeignKey('PaymentPlans.id'), nullable=True) Invoices_to_Pay = db.Column(db.String()) @@ -86,7 +86,7 @@ class SinglePayments(db.Model): Refund_JSON = db.Column(db.Text()) Stripe_Refund_ID = db.Column(db.String()) Stripe_Refund_Created = db.Column(db.DateTime, nullable=True) - Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) Invoices_to_Pay = db.Column(db.String()) @@ -111,6 +111,6 @@ class PaymentPlans(db.Model): Frequency = db.Column(db.String(50)) Start_Date = db.Column(db.DateTime, nullable=True) Stripe_Payment_Method = db.Column(db.String(50)) - Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Created = db.Column(db.DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) Enabled = db.Column(db.Boolean, nullable=True, default=True) \ No newline at end of file diff --git a/payment_processors/batch_processor.py b/payment_processors/batch_processor.py index d4c39b4..3fcef7e 100644 --- a/payment_processors/batch_processor.py +++ b/payment_processors/batch_processor.py @@ -152,7 +152,7 @@ class BatchPaymentProcessor(BasePaymentProcessor): else: #self.logger.info(f"No customers found for {payment_method_names[pm]}") self.logger.info(f"No customers found for {str(payment_methods)}") - sys.exit() + #sys.exit() return batch_ids def _execute_payment_batches(self, batch_ids: List[int]) -> tuple: diff --git a/static/css/custom.css b/static/css/custom.css index 202a126..aba2b28 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,4 @@ -/* Custom CSS for Plutus - God of Wealth Theme */ +/* Custom CSS for Plutus - God of Wealth Theme - Bootstrap 5 Compatible */ /* Plutus-inspired theme colors extracted from the god image */ :root { @@ -16,97 +16,187 @@ --plutus-danger: #dc143c; } + +.modal-backdrop { + z-index: 1050 !important; /* Bootstrap order */ + position: fixed !important; + pointer-events: auto; /* backdrop blocks the page */ +} +/* Modal container sits above backdrop; keep it transparent */ +.modal { + z-index: 1055 !important; /* above backdrop */ + position: fixed !important; + background-color: transparent !important; /* backdrop provides the dim */ + pointer-events: none; /* container shouldn't intercept */ +} +/* Dialog/content are interactive */ +.modal .modal-dialog, +.modal .modal-content { + position: relative; + z-index: 1056; /* top of the small stack */ + pointer-events: auto; +} + +/* Remove accidental stacking contexts that can trap modals */ +.container, .container-fluid, +main, .footer, .navbar { + z-index: auto !important; +} + + + /* Custom navbar styling with Plutus theme */ -.navbar.is-dark { - background: linear-gradient(135deg, var(--plutus-deep-navy) 0%, var(--plutus-charcoal) 100%); +.navbar.navbar-dark { + background: linear-gradient(135deg, var(--plutus-deep-navy) 0%, var(--plutus-charcoal) 100%) !important; border-bottom: 2px solid var(--plutus-gold); box-shadow: 0 2px 10px rgba(212, 175, 55, 0.3); } -.navbar-brand .navbar-item { +.navbar-brand { font-weight: 700; font-size: 1.2rem; color: var(--plutus-gold) !important; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); } -.navbar-item { +.navbar-dark .nav-link { color: var(--plutus-warm-white) !important; transition: color 0.3s ease, background-color 0.3s ease; } -.navbar-item:hover { +.navbar-dark .nav-link:hover, .navbar-dark .nav-link:focus { color: var(--plutus-amber) !important; - background-color: rgba(212, 175, 55, 0.1) !important; -} - -.navbar-link { - color: var(--plutus-warm-white) !important; -} - -.navbar-link:hover { - color: var(--plutus-amber) !important; - background-color: rgba(212, 175, 55, 0.1) !important; + background-color: rgba(212, 175, 55, 0.1); } /* Navbar dropdown styling */ -.navbar-dropdown { +.dropdown-menu { background-color: var(--plutus-deep-navy) !important; - border-color: var(--plutus-gold) !important; + border: 1px solid var(--plutus-gold) !important; box-shadow: 0 8px 16px rgba(212, 175, 55, 0.2) !important; } -.navbar-dropdown .navbar-item { +.dropdown-item { color: var(--plutus-warm-white) !important; - background-color: transparent !important; } -.navbar-dropdown .navbar-item:hover { +.dropdown-item:hover, .dropdown-item:focus { color: var(--plutus-amber) !important; background-color: rgba(212, 175, 55, 0.1) !important; } -/* Hero section customization */ -.hero.is-primary { +.dropdown-divider { + border-top-color: var(--plutus-gold) !important; + opacity: 0.5; +} + +/* Hero/Jumbotron section */ +.hero { background: linear-gradient(135deg, var(--plutus-gold) 0%, var(--plutus-amber) 100%); color: var(--plutus-charcoal); + padding: 3rem 2rem; + border-radius: 8px; + margin-bottom: 2rem; +} + +/* Card styling (replaces .box from Bulma) */ +.card { + background-color: rgba(250, 248, 240, 0.95); + border: 1px solid rgba(212, 175, 55, 0.3); + box-shadow: 0 0.5em 1em -0.125em rgba(212, 175, 55, 0.2); + backdrop-filter: blur(5px); +} + +.card:hover { + box-shadow: 0 0.5em 1.5em -0.125em rgba(212, 175, 55, 0.3); + border-color: rgba(212, 175, 55, 0.5); + transition: all 0.3s ease; +} + +.card-body { + background-color: rgba(250, 248, 240, 0.95); } -/* Content boxes with Plutus theme */ +/* Legacy .box support for templates not yet migrated */ .box { background-color: rgba(250, 248, 240, 0.95); border: 1px solid rgba(212, 175, 55, 0.3); - box-shadow: 0 0.5em 1em -0.125em rgba(212, 175, 55, 0.2), 0 0px 0 1px rgba(212, 175, 55, 0.1); + box-shadow: 0 0.5em 1em -0.125em rgba(212, 175, 55, 0.2); backdrop-filter: blur(5px); + padding: 1.25rem; + border-radius: 6px; } .box:hover { - box-shadow: 0 0.5em 1.5em -0.125em rgba(212, 175, 55, 0.3), 0 0px 0 1px rgba(212, 175, 55, 0.2); + box-shadow: 0 0.5em 1.5em -0.125em rgba(212, 175, 55, 0.3); border-color: rgba(212, 175, 55, 0.5); transition: all 0.3s ease; } /* Button styling with Plutus theme */ -.button.is-primary { +.btn-primary { background-color: var(--plutus-gold); border-color: var(--plutus-rich-gold); color: var(--plutus-charcoal); font-weight: 600; } -.button.is-primary:hover { +.btn-primary:hover, .btn-primary:focus { background-color: var(--plutus-amber); border-color: var(--plutus-gold); + color: var(--plutus-charcoal); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(212, 175, 55, 0.4); } -.button:hover { +.btn:hover { transform: translateY(-1px); transition: all 0.2s ease; } +/* Legacy button support */ +.button.is-primary { + background-color: var(--plutus-gold); + border-color: var(--plutus-rich-gold); + color: var(--plutus-charcoal); + font-weight: 600; + padding: 0.5rem 1rem; + border-radius: 4px; + border-width: 1px; + border-style: solid; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.button.is-primary:hover { + background-color: var(--plutus-amber); + border-color: var(--plutus-gold); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(212, 175, 55, 0.4); +} + +.button.is-warning { + background-color: var(--plutus-warning); + border-color: var(--plutus-dark-bronze); + color: white; +} + +.button.is-light { + background-color: #f5f5f5; + border-color: #dbdbdb; + color: #363636; +} + /* Table enhancements with Plutus theme */ +.table-responsive { + border-radius: 8px; + box-shadow: 0 4px 15px rgba(212, 175, 55, 0.2); + border: 1px solid rgba(212, 175, 55, 0.3); +} + +/* Legacy support */ .table-container { overflow-x: auto; border-radius: 8px; @@ -130,7 +220,7 @@ border-color: rgba(212, 175, 55, 0.2); } -.table tr:hover { +.table-hover tbody tr:hover { background-color: rgba(212, 175, 55, 0.1); } @@ -138,16 +228,41 @@ .footer { background: linear-gradient(135deg, var(--plutus-deep-navy) 0%, var(--plutus-charcoal) 100%); color: var(--plutus-warm-white); - padding: 2rem 1.5rem; - margin-top: 2rem; border-top: 2px solid var(--plutus-gold); } -/* Notification improvements with theme */ +/* Alert styling (replaces .notification from Bulma) */ +.alert { + border-radius: 8px; + backdrop-filter: blur(5px); + border: 1px solid rgba(212, 175, 55, 0.3); +} + +.alert-success { + background-color: rgba(34, 139, 34, 0.9); + color: var(--plutus-warm-white); + border-color: var(--plutus-success); +} + +.alert-danger { + background-color: rgba(220, 20, 60, 0.9); + color: var(--plutus-warm-white); + border-color: var(--plutus-danger); +} + +.alert-info { + background-color: rgba(212, 175, 55, 0.9); + color: var(--plutus-charcoal); + border-color: var(--plutus-gold); +} + +/* Legacy notification support */ .notification { border-radius: 8px; backdrop-filter: blur(5px); border: 1px solid rgba(212, 175, 55, 0.3); + padding: 1.25rem 2.5rem 1.25rem 1.5rem; + position: relative; } .notification.is-success { @@ -165,11 +280,29 @@ color: var(--plutus-charcoal); } +.notification.is-light { + background-color: rgba(245, 245, 245, 0.95); + color: #363636; +} + /* Form styling with Plutus theme */ +.form-control:focus, .form-select:focus { + border-color: var(--plutus-gold); + box-shadow: 0 0 0 0.25rem rgba(212, 175, 55, 0.25); +} + +.form-control, .form-select { + background-color: rgba(250, 248, 240, 0.95); + border-color: rgba(212, 175, 55, 0.4); +} + +/* Legacy field/control/input support */ .field .control .input:focus, -.field .control .textarea:focus { +.field .control .textarea:focus, +.field .control .select select:focus { border-color: var(--plutus-gold); box-shadow: 0 0 0 0.125em rgba(212, 175, 55, 0.25); + outline: none; } .field .control .input, @@ -177,474 +310,456 @@ .field .control .select select { background-color: rgba(250, 248, 240, 0.95); border-color: rgba(212, 175, 55, 0.4); + border-width: 1px; + border-style: solid; + border-radius: 4px; + padding: 0.5rem 0.75rem; + width: 100%; +} + +.field .control .select { + display: inline-block; + max-width: 100%; + position: relative; + vertical-align: top; + width: 100%; +} + +.field .control .select.is-fullwidth { + width: 100%; } -/* Tags with Plutus theme */ +.field .control .select select { + cursor: pointer; + display: block; + font-size: 1rem; + max-width: 100%; + outline: none; +} + +.label, .form-label { + color: var(--plutus-charcoal); + font-weight: 600; +} + +/* Badge styling (replaces .tag from Bulma) */ +.badge.bg-success { + background-color: var(--plutus-success) !important; +} + +.badge.bg-warning { + background-color: var(--plutus-warning) !important; + color: var(--plutus-charcoal) !important; +} + +.badge.bg-danger { + background-color: var(--plutus-danger) !important; +} + +.badge.bg-info { + background-color: var(--plutus-gold) !important; + color: var(--plutus-charcoal) !important; +} + +/* Legacy tag support */ .tag.is-success { background-color: var(--plutus-success); color: var(--plutus-warm-white); + padding: 0.25rem 0.75rem; + border-radius: 4px; + display: inline-block; + font-size: 0.875rem; } .tag.is-warning { background-color: var(--plutus-warning); color: var(--plutus-charcoal); + padding: 0.25rem 0.75rem; + border-radius: 4px; + display: inline-block; + font-size: 0.875rem; } .tag.is-danger { background-color: var(--plutus-danger); color: var(--plutus-warm-white); + padding: 0.25rem 0.75rem; + border-radius: 4px; + display: inline-block; + font-size: 0.875rem; } .tag.is-info { background-color: var(--plutus-gold); color: var(--plutus-charcoal); + padding: 0.25rem 0.75rem; + border-radius: 4px; + display: inline-block; + font-size: 0.875rem; } -/* Plutus Background Implementation */ -body { - background-image: url('../images/plutus3.JPG'); - background-size: cover; - background-position: center; - background-attachment: fixed; - background-repeat: no-repeat; - position: relative; -} - -/* Dark overlay for better content readability */ -body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient( - 135deg, - rgba(26, 26, 46, 0.85) 0%, - rgba(44, 44, 44, 0.75) 50%, - rgba(26, 26, 46, 0.85) 100% - ); - z-index: -1; - pointer-events: none; -} - -/* Content area styling for better readability over background */ -main.section { - position: relative; - z-index: 1; +.tag.is-light { + background-color: #f5f5f5; + color: #363636; + padding: 0.25rem 0.75rem; + border-radius: 4px; + display: inline-block; + font-size: 0.875rem; } -.container { - max-width: 1488px !important; /* 20% wider than Bulma's default 1240px */ - position: relative; - z-index: 2; +.tag.is-small { + font-size: 0.75rem; + padding: 0.15rem 0.5rem; } -/* Title styling with Plutus theme */ -.title { - color: var(--plutus-gold); - font-weight: 700; +/* Body and background styling */ +body { + background: linear-gradient(to bottom, rgba(26, 26, 46, 0.05) 0%, rgba(212, 175, 55, 0.05) 100%); + background-attachment: fixed; + min-height: 100vh; + color: var(--plutus-charcoal); } -.subtitle { - color: var(--plutus-charcoal); +/* Container customization */ +.container { + max-width: 1488px; } /* Breadcrumb styling */ .breadcrumb { - background-color: rgba(250, 248, 240, 0.9); - border-radius: 6px; - border: 1px solid rgba(212, 175, 55, 0.3); + background-color: transparent; padding: 0.75rem 1rem; - backdrop-filter: blur(5px); -} - -.breadcrumb a { - color: var(--plutus-bronze); + border-radius: 6px; + margin-bottom: 1rem; } -.breadcrumb a:hover { +.breadcrumb-item.active { color: var(--plutus-gold); } -.breadcrumb .is-active a { - color: var(--plutus-charcoal); +.breadcrumb-item + .breadcrumb-item::before { + color: var(--plutus-gold); } -/* Level component styling */ +/* Level component (horizontal layout) - legacy support */ .level { - background-color: rgba(250, 248, 240, 0.85); - border-radius: 6px; - padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 1.5rem; - border: 1px solid rgba(212, 175, 55, 0.3); - backdrop-filter: blur(5px); } -/* Modal styling with Plutus theme */ -.modal-card-head { - background-color: var(--plutus-gold); - color: var(--plutus-charcoal); +.level-left, .level-right { + display: flex; + align-items: center; + gap: 0.75rem; } -.modal-card-body { - background-color: var(--plutus-warm-white); +/* Modal customization */ +.modal-content { + background-color: rgba(250, 248, 240, 0.98); + border: 1px solid rgba(212, 175, 55, 0.3); + box-shadow: 0 10px 40px rgba(212, 175, 55, 0.3); } -.modal-card-foot { - background-color: var(--plutus-cream); +.modal-header { + border-bottom-color: rgba(212, 175, 55, 0.3); } -/* Code blocks styling */ -pre { - background-color: var(--plutus-charcoal) !important; - color: var(--plutus-cream) !important; - border: 1px solid var(--plutus-gold); +.modal-footer { + border-top-color: rgba(212, 175, 55, 0.3); } -code { - background-color: rgba(44, 44, 44, 0.9) !important; - color: var(--plutus-amber) !important; - padding: 0.2em 0.4em; - border-radius: 3px; +.modal-header.bg-warning { + background-color: var(--plutus-warning) !important; + color: var(--plutus-charcoal); } -/* Dashboard-specific styling - remove background image */ -.dashboard-page body { - background-image: none !important; - background-color: var(--plutus-warm-white); +.modal-header.bg-success { + background-color: var(--plutus-success) !important; + color: white; } -.dashboard-page body::before { - display: none !important; +.modal-header.bg-danger { + background-color: var(--plutus-danger) !important; + color: white; } -/* Plutus image styling for dashboard */ -.plutus-image { - width: 100%; - height: auto; - max-width: 1000px; - border-radius: 12px; - box-shadow: 0 8px 32px rgba(212, 175, 55, 0.4); - border: 3px solid var(--plutus-gold); - margin: 2rem auto; - display: block; -} +/* Legacy modal support (Bulma-style) */ -/* Payment Status Colors */ -:root { - --status-success: #90ee90; /* Light green for Success */ - --status-pending: #f5deb3; /* Light mustard for Pending */ - --status-refund: #dda0dd; /* Light purple for Refund */ - --status-failed: #ffcccb; /* Light red for Failed */ -} +/* Backdrop blocks the page */ -/* Payment Status Badges */ -.status-badge { - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 600; - text-align: center; - display: inline-flex; - align-items: center; - gap: 0.375rem; - border: 1px solid transparent; -} -.status-badge.success { - background-color: var(--status-success); - color: #2d5016; - border-color: #7ab317; +/* Backdrop sits under modal container */ +.modal-backdrop { + z-index: 1050 !important; /* Bootstrap default order */ + position: fixed !important; } -.status-badge.pending { - background-color: var(--status-pending); - color: #8b4513; - border-color: #daa520; +/* Modal container sits above backdrop; keep it transparent */ +.modal { + z-index: 1055 !important; /* above backdrop */ + position: fixed !important; + background-color: transparent !important; /* backdrop provides the dim */ } -.status-badge.refund { - background-color: var(--status-refund); - color: #4b0082; - border-color: #9370db; +/* Dialog/content are interactive */ +.modal .modal-dialog, +.modal .modal-content { + position: relative; + z-index: 1056; } -.status-badge.failed { - background-color: var(--status-failed); - color: #8b0000; - border-color: #dc143c; -} +/* Optional: ensure dropdowns/nav still layer correctly */ +.navbar { z-index: 1030 !important; } +.navbar .dropdown-menu { z-index: 1031 !important; } -/* Status Icons */ -.status-badge i.fas { - font-size: 0.875rem; +/* Remove accidental stacking contexts that could trap modals */ +.container, .container-fluid, +main, .footer, .navbar { + z-index: auto !important; /* prevents parent stacks (e.g., z-index:1) */ } -/* Hover effects for clickable status badges */ -.status-badge.clickable { - cursor: pointer; - transition: all 0.2s ease; -} -.status-badge.clickable:hover { - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +.modal-card-title { + color: currentColor; + flex-grow: 1; + flex-shrink: 0; + font-size: 1.5rem; + line-height: 1; + margin: 0; } -/* Error Alert Styling */ -.error-alert { - margin-bottom: 0.5rem; - padding: 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - border: 1px solid; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +.modal-card-body { + flex-grow: 1; + flex-shrink: 1; + overflow: auto; + padding: 20px; + background-color: rgba(250, 248, 240, 0.98); } -.error-alert-header { - display: flex; - align-items: center; - margin-bottom: 0.5rem; - font-weight: 600; +.modal-card-foot { + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + border-top: 1px solid rgba(212, 175, 55, 0.3); + justify-content: flex-end; } -.error-alert-header .icon { - margin-right: 0.5rem; +.delete { + background-color: rgba(10, 10, 10, 0.2); + border: none; + border-radius: 290486px; + cursor: pointer; + pointer-events: auto; + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + font-size: 0; + height: 20px; + max-height: 20px; + max-width: 20px; + min-height: 20px; + min-width: 20px; + outline: none; + position: relative; + vertical-align: top; + width: 20px; } -.error-alert-body { - margin-left: 1.5rem; +.delete::before, .delete::after { + background-color: white; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } -.error-message { - margin-bottom: 0.25rem; - font-weight: 500; +.delete::before { + height: 2px; + width: 50%; } -.error-suggestion { - margin-bottom: 0.5rem; - font-size: 0.8rem; - opacity: 0.9; +.delete::after { + height: 50%; + width: 2px; } -.error-details { - margin-top: 0.5rem; +.delete:hover { + background-color: rgba(10, 10, 10, 0.3); } -.error-details summary { - cursor: pointer; - font-size: 0.75rem; - color: #666; - margin-bottom: 0.25rem; +/* Custom status badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-weight: 600; + font-size: 0.875rem; + display: inline-block; } -.error-details pre { - background-color: rgba(0, 0, 0, 0.05); - padding: 0.5rem; - border-radius: 0.25rem; - font-size: 0.7rem; - max-height: 100px; - overflow-y: auto; +.status-badge.success { + background-color: var(--plutus-success); + color: white; } -/* Error Type Specific Styling */ -.error-alert.insufficient-funds { - background-color: #ffe4e1; - color: #8b0000; - border-color: #dc143c; +.status-badge.failed { + background-color: var(--plutus-danger); + color: white; } -.error-alert.insufficient-funds .icon { - color: #dc143c; +.status-badge.pending { + background-color: var(--plutus-warning); + color: var(--plutus-charcoal); } -.error-alert.incorrect-card { - background-color: #fff8dc; - color: #8b4513; - border-color: #daa520; +/* Custom spinner */ +.spinner { + display: inline-block; + width: 40px; + height: 40px; + border: 4px solid rgba(212, 175, 55, 0.3); + border-radius: 50%; + border-top-color: var(--plutus-gold); + animation: spin 1s ease-in-out infinite; } -.error-alert.incorrect-card .icon { - color: #daa520; +@keyframes spin { + to { transform: rotate(360deg); } } -.error-alert.general-decline { - background-color: #ffebcd; - color: #a0522d; - border-color: #cd853f; +/* Utility classes to match Bulma */ +.is-hidden { + display: none !important; } -.error-alert.general-decline .icon { - color: #cd853f; +.has-text-centered, .text-center { + text-align: center !important; } -.error-alert.bank-contact { - background-color: #e6e6fa; - color: #4b0082; - border-color: #9370db; +.has-text-weight-bold { + font-weight: 700 !important; } -.error-alert.bank-contact .icon { - color: #9370db; +.has-text-weight-semibold { + font-weight: 600 !important; } -.error-alert.processing-error { - background-color: #e0f6ff; - color: #006b96; - border-color: #0088cc; +.has-text-grey { + color: #7a7a7a !important; } -.error-alert.processing-error .icon { - color: #0088cc; +.has-background-light { + background-color: #f5f5f5 !important; } -.error-alert.network-error { - background-color: #f0f0f0; - color: #555; - border-color: #999; +.is-size-7 { + font-size: 0.875rem !important; } -.error-alert.network-error .icon { - color: #999; +.is-fullwidth { + width: 100% !important; } -/* Compact Error Alert for Table Rows */ -.error-alert-compact { +/* Icon spacing */ +.icon { display: inline-flex; align-items: center; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; - font-weight: 500; - border: 1px solid; - margin-right: 0.25rem; -} - -.error-alert-compact .icon { - margin-right: 0.25rem; - font-size: 0.7rem; -} - -.error-alert-compact.insufficient-funds { - background-color: #ffe4e1; - color: #8b0000; - border-color: #dc143c; + justify-content: center; + height: 1.5rem; + width: 1.5rem; } -.error-alert-compact.incorrect-card { - background-color: #fff8dc; - color: #8b4513; - border-color: #daa520; +.icon.is-small { + height: 1rem; + width: 1rem; } -.error-alert-compact.general-decline { - background-color: #ffebcd; - color: #a0522d; - border-color: #cd853f; +.icon.is-left { + margin-right: 0.5rem; } -.error-alert-compact.bank-contact { - background-color: #e6e6fa; - color: #4b0082; - border-color: #9370db; +/* Field grouping */ +.field.is-grouped { + display: flex; + gap: 0.5rem; } -.error-alert-compact.processing-error { - background-color: #e0f6ff; - color: #006b96; - border-color: #0088cc; +.field .control { + position: relative; } -.error-alert-compact.network-error { - background-color: #f0f0f0; - color: #555; - border-color: #999; +.field.has-addons { + display: flex; } -/* Payment Method Management Styles */ -.payment-method-card { - transition: all 0.3s ease; - cursor: pointer; +.field.has-addons .control { + flex-grow: 1; } -.payment-method-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +/* Typography */ +.title { + font-weight: 600; + line-height: 1.125; + margin-bottom: 1rem; + color: var(--plutus-charcoal); } -.payment-method-selected { - border-color: var(--plutus-blue) !important; - background-color: rgba(50, 115, 220, 0.1) !important; +.title.is-4 { + font-size: 1.5rem; } -.stripe-elements-container { - border: 1px solid #dbdbdb; - border-radius: 4px; - padding: 12px; - background-color: white; - min-height: 48px; +.title.is-5 { + font-size: 1.25rem; } -.payment-method-type-icon { - font-size: 2.5rem; - margin-bottom: 1rem; +.subtitle { + font-weight: 400; + line-height: 1.25; + color: #4a4a4a; } -.setup-progress { - margin-bottom: 2rem; +.subtitle.is-5 { + font-size: 1.25rem; } -.setup-step { - display: flex; - align-items: center; - margin-bottom: 0.5rem; +/* Content formatting */ +.content { + line-height: 1.7; } -.setup-step-number { - width: 30px; - height: 30px; - border-radius: 50%; - background-color: #dbdbdb; - color: white; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; - font-weight: bold; +.help { + display: block; + font-size: 0.875rem; + margin-top: 0.25rem; + color: #7a7a7a; } -.setup-step.is-active .setup-step-number { - background-color: var(--plutus-gold); -} -.setup-step.is-completed .setup-step-number { - background-color: var(--plutus-success); -} +/* --- Bootstrap 5 modal fixes --- */ -.payment-method-summary { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - border-left: 4px solid var(--plutus-gold); -} -/* Success animations */ -.success-checkmark { - animation: checkmark 0.6s ease-in-out; -} +/* Keep Bootstrap’s stacking order (backdrop under modal): */ -@keyframes checkmark { - 0% { transform: scale(0); opacity: 0; } - 50% { transform: scale(1.2); opacity: 1; } - 100% { transform: scale(1); opacity: 1; } -} -/* Responsive improvements */ +/* Responsive utilities */ @media (max-width: 768px) { - .payment-type-selection .column { - margin-bottom: 1rem; + .container { + padding-left: 1rem; + padding-right: 1rem; } - - .setup-progress { - font-size: 0.9rem; + + .level { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .level-left, .level-right { + width: 100%; + flex-direction: column; + align-items: flex-start; } -} \ No newline at end of file +} diff --git a/templates/analytics/dashboard.html b/templates/analytics/dashboard.html index 15728ac..0f06aa8 100644 --- a/templates/analytics/dashboard.html +++ b/templates/analytics/dashboard.html @@ -4,6 +4,58 @@ {% block head %} {% endblock %} {% block content %} -
-
-
-
-
-

- - - - Analytics Dashboard -

-
-
-
-
-
-

- -

-

- -

-
-
-
+
+
+
+

+ Analytics Dashboard +

+
+
+ +
+
- -
-
-
-
- -- -
-
System Health Score
-

Overall system performance

-
-
-
-
-
--%
-
Payment Success Rate
-

Last 24 hours

+ +
+
+
+
+ --
+
System Health Score
+

Overall system performance

-
-
-
--%
-
Error Rate
-

System errors in logs

-
+
+
+
+
--%
+
Payment Success Rate
+

Last 24 hours

-
-
-
--
-
Total Payments
-

Recent activity

-
+
+
+
+
--%
+
Error Rate
+

System errors in logs

- - -
- +
+
+
--
+
Total Payments
+

Recent activity

+
+
- -
- -
-
-

- - - - System Performance + + + + +
+ +
+
+
+

+ System Performance

-
- - - -

Loading performance metrics...

+
+
+ Loading... +
+

Loading performance metrics...

+
- -

+ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/auth/add_user.html b/templates/auth/add_user.html index 1aefacd..fbe6215 100644 --- a/templates/auth/add_user.html +++ b/templates/auth/add_user.html @@ -2,74 +2,120 @@ {% block title %}Add User - Plutus{% endblock %} +{% block head %} + +{% endblock %} + {% block content %} -
-
-
-

Add New User

- -
-
- -
- - - - +
+
+
+
+

Add New User

+ + +
+ +
+ + +
-
- -
- -
- - - - + +
+ +
+ + +
-
- -
- -
- - - - + +
+ +
+ + +
-
- -
- -
- - - - + +
+ +
+ + +
-
-
- -
- +
+ +
-
- -
-
- + Cancel
-
- Cancel -
-
- + +
diff --git a/templates/auth/list_users.html b/templates/auth/list_users.html index 50105d0..794f3af 100644 --- a/templates/auth/list_users.html +++ b/templates/auth/list_users.html @@ -2,24 +2,83 @@ {% block title %}Users - Plutus{% endblock %} +{% block head %} + +{% endblock %} + {% block content %} -
-
-

Users

-
- +
+

Users

+ + Add User +
{% if users %} -
- +
+
@@ -39,7 +98,7 @@ @@ -51,8 +110,8 @@
ID{{ user.FullName }} {{ user.Email }} - + {{ 'Active' if user.Enabled else 'Disabled' }}
{% else %} -
-

No users found. Add the first user.

+
+

No users found. Add the first user.

{% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/auth/login.html b/templates/auth/login.html index 7512a32..84df999 100644 --- a/templates/auth/login.html +++ b/templates/auth/login.html @@ -2,44 +2,98 @@ {% block title %}Login - Plutus{% endblock %} +{% block head %} + +{% endblock %} + {% block content %} -
-
-
-

Login to Plutus

- -
-
- -
- - - - +
+
+
+
+

Login to Plutus

+ + +
+ +
+ + +
-
- -
- -
- - - - + +
+ +
+ + +
-
- -
-
-
-
- + +
diff --git a/templates/base.html b/templates/base.html index 2eb3ac5..6a465b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,165 +4,123 @@ {% block title %}Plutus{% endblock %} - + {% block head %}{% endblock %} - - {% endif %} - - {% else %} -
-

No log entries found.

- {% endif %}
-
@@ -280,7 +316,7 @@ document.addEventListener('DOMContentLoaded', function() { function initializeLogs() { const tableBody = document.getElementById('logsTableBody'); const rows = tableBody.querySelectorAll('tr'); - + allLogs = Array.from(rows).map(row => { const cells = row.querySelectorAll('td'); return { @@ -293,7 +329,7 @@ function initializeLogs() { ipAddress: cells[5] ? (cells[5].textContent.trim() || '') : '' }; }); - + filteredLogs = [...allLogs]; updateResultCount(); } @@ -314,26 +350,26 @@ function applyFilters() { const entityTypeFilter = document.getElementById('entityTypeFilter').value; const dateFromFilter = document.getElementById('dateFromFilter').value; const dateToFilter = document.getElementById('dateToFilter').value; - + // Filter logs filteredLogs = allLogs.filter(log => { // Search filter - const searchMatch = !searchTerm || + const searchMatch = !searchTerm || log.user.toLowerCase().includes(searchTerm) || log.action.toLowerCase().includes(searchTerm) || log.entityType.toLowerCase().includes(searchTerm) || log.details.toLowerCase().includes(searchTerm) || log.ipAddress.toLowerCase().includes(searchTerm); - + // User filter const userMatch = !userFilter || log.user.includes(`ID: ${userFilter}`); - + // Action filter const actionMatch = !actionFilter || log.action.includes(actionFilter); - + // Entity type filter const entityTypeMatch = !entityTypeFilter || log.entityType.includes(entityTypeFilter); - + // Date filters would need server-side implementation for full functionality // For now, we'll implement basic client-side date filtering on visible text let dateMatch = true; @@ -342,10 +378,10 @@ function applyFilters() { if (dateFromFilter && logDate < dateFromFilter) dateMatch = false; if (dateToFilter && logDate > dateToFilter) dateMatch = false; } - + return searchMatch && userMatch && actionMatch && entityTypeMatch && dateMatch; }); - + // Update display updateTable(); updateResultCount(); @@ -353,12 +389,12 @@ function applyFilters() { function updateTable() { const tableBody = document.getElementById('logsTableBody'); - + // Hide all rows first allLogs.forEach(log => { log.element.style.display = 'none'; }); - + // Show filtered rows filteredLogs.forEach(log => { log.element.style.display = ''; @@ -369,9 +405,9 @@ function updateTable() { function updateResultCount() { const resultCount = document.getElementById('resultCount'); const filterResults = document.getElementById('filterResults'); - + resultCount.textContent = filteredLogs.length; - + if (filteredLogs.length === allLogs.length) { filterResults.style.display = 'none'; } else { @@ -397,8 +433,8 @@ function showLogDetail(logId) { if (data.success) { const log = data.log; const detailHtml = ` -
- +
+
@@ -430,20 +466,23 @@ function showLogDetail(logId) {
ID
- + ${log.Log_Entry ? ` -
- -
-
${log.Log_Entry}
+
+ +
+
+
${log.Log_Entry}
+
` : ''}
`; - + document.getElementById('logDetailContent').innerHTML = detailHtml; - document.getElementById('logDetailModal').classList.add('is-active'); + const modal = new bootstrap.Modal(document.getElementById('logDetailModal')); + modal.show(); } else { alert('Failed to load log details: ' + data.error); } @@ -454,22 +493,20 @@ function showLogDetail(logId) { }); } -function hideModal(modalId) { - document.getElementById(modalId).classList.remove('is-active'); -} - function copyLogDetails() { const content = document.getElementById('logDetailContent').innerText; navigator.clipboard.writeText(content).then(function() { // Show temporary success message const button = event.target.closest('button'); const originalText = button.innerHTML; - button.innerHTML = 'Copied!'; - button.classList.add('is-success'); - + button.innerHTML = ' Copied!'; + button.classList.remove('btn-info'); + button.classList.add('btn-success'); + setTimeout(function() { button.innerHTML = originalText; - button.classList.remove('is-success'); + button.classList.remove('btn-success'); + button.classList.add('btn-info'); }, 2000); }).catch(function(err) { console.error('Failed to copy text: ', err); @@ -480,31 +517,23 @@ function copyLogDetails() { function exportLogs() { // Create form data with current filters const params = new URLSearchParams(); - + const searchTerm = document.getElementById('searchInput').value; const userFilter = document.getElementById('userFilter').value; const actionFilter = document.getElementById('actionFilter').value; const entityTypeFilter = document.getElementById('entityTypeFilter').value; const dateFromFilter = document.getElementById('dateFromFilter').value; const dateToFilter = document.getElementById('dateToFilter').value; - + if (searchTerm) params.append('search', searchTerm); if (userFilter) params.append('user', userFilter); if (actionFilter) params.append('action', actionFilter); if (entityTypeFilter) params.append('entity_type', entityTypeFilter); if (dateFromFilter) params.append('date_from', dateFromFilter); if (dateToFilter) params.append('date_to', dateToFilter); - + // Open export URL in new window window.open(`/logs/export?${params.toString()}`, '_blank'); } - -// Close modal on Escape key -document.addEventListener('keydown', function(event) { - if (event.key === 'Escape') { - const activeModals = document.querySelectorAll('.modal.is-active'); - activeModals.forEach(modal => modal.classList.remove('is-active')); - } -}); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/main/payment_detail.html b/templates/main/payment_detail.html index d5b8794..f9b9e95 100644 --- a/templates/main/payment_detail.html +++ b/templates/main/payment_detail.html @@ -1,718 +1,525 @@ {% extends "base.html" %} -{% block title %}Payment #{{ payment.id }} - Plutus{% endblock %} +{% block title %}Payment Plan #{{ plan.id }} - Plutus{% endblock %} + +{% block head %} + + + +{% endblock %} {% block content %} -