27 changed files with 8711 additions and 3985 deletions
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -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 }} |
|||
{% 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> |
|||
@ -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> |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -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 %} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue