You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

863 lines
34 KiB

{% extends "base.html" %}
{% block title %}Single Payments - 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 on top of background */
.container-fluid, .container {
position: relative;
z-index: 1;
}
/* Ensure navbar and footer are above background */
.navbar, .footer, main {
position: relative;
z-index: 1;
}
/* Page title and breadcrumb styling */
h1, h2, h3, h4, h5, h6 {
color: #faf8f0 !important;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.breadcrumb-item a {
color: #d4af37 !important;
}
.breadcrumb-item.active {
color: #faf8f0 !important;
}
/* Enhanced visibility */
.card {
background-color: rgba(250, 248, 240, 0.98) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.table-responsive {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.alert {
background-color: rgba(250, 248, 240, 0.98);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
/* Modal styling to match Hades theme */
/* Z-index is handled globally in custom.css */
.modal-content {
background-color: #2b2b2b;
border: none;
border-radius: 8px;
}
.modal-header {
background-color: #d4af37;
color: #000;
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
padding: 1rem 1.5rem;
}
.modal-header .modal-title {
color: #000;
font-weight: 600;
}
.modal-header .btn-close {
filter: brightness(0);
opacity: 0.7;
}
.modal-header .btn-close:hover {
opacity: 1;
}
.modal-body {
background-color: #2b2b2b;
color: #f0f0f0;
padding: 1.5rem;
max-height: 70vh;
overflow-y: auto;
}
.modal-body pre {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 1rem;
}
.modal-body pre code {
color: inherit;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
/* JSON syntax highlighting */
.modal-body pre code .hljs-attr,
.modal-body pre code .hljs-string {
color: #ce9178;
}
.modal-body pre code .hljs-number {
color: #b5cea8;
}
.modal-body pre code .hljs-literal {
color: #569cd6;
}
/* Copy button styling inside modal */
.modal-body .btn-info,
.modal-body .btn-primary,
.modal-body .btn-outline-secondary {
background-color: #d4af37;
border-color: #d4af37;
color: #000;
}
.modal-body .btn-info:hover,
.modal-body .btn-primary:hover,
.modal-body .btn-outline-secondary:hover {
background-color: #c5a028;
border-color: #c5a028;
color: #000;
}
.modal-body .btn-success {
background-color: #28a745;
border-color: #28a745;
color: #fff;
}
/* Error modal specific styling */
.modal-header.bg-danger {
background-color: #dc3545 !important;
color: #fff !important;
}
.modal-header.bg-danger .modal-title {
color: #fff;
}
.modal-header.bg-danger .btn-close {
filter: brightness(0) invert(1);
}
/* Scrollbar styling for dark modal */
.modal-body::-webkit-scrollbar {
width: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: #1e1e1e;
border-radius: 4px;
}
.modal-body::-webkit-scrollbar-thumb {
background: #d4af37;
border-radius: 4px;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: #c5a028;
}
/* Page title styling */
.page-title {
background: linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(255, 191, 0, 0.95) 100%);
color: #2c2c2c;
padding: 1.5rem 2rem;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(212, 175, 55, 0.4);
margin-bottom: 1.5rem;
}
</style>
<script>
// Payment filtering and sorting functionality
let allPayments = [];
let filteredPayments = [];
// Initialize payment data and filters when page loads
document.addEventListener('DOMContentLoaded', function() {
console.log('Single Payments List: DOM loaded, initializing payment filtering and modals');
initializePayments();
populatePaymentMethodFilter();
setupEventListeners();
});
function initializePayments() {
const tableBody = document.getElementById('paymentsTableBody');
if (!tableBody) {
console.error('Single Payments List: Table body not found');
return;
}
const rows = tableBody.querySelectorAll('tr');
allPayments = Array.from(rows).map(row => {
const cells = row.querySelectorAll('td');
return {
element: row,
paymentId: cells[0] ? (cells[0].textContent.trim() || '') : '',
date: cells[1] ? (cells[1].textContent.trim() || '') : '',
splynxId: cells[2] ? (cells[2].textContent.trim() || '') : '',
stripeCustomerId: cells[3] ? (cells[3].textContent.trim() || '') : '',
paymentIntent: cells[4] ? (cells[4].textContent.trim() || '') : '',
paymentMethod: cells[5] ? (cells[5].textContent.trim() || '') : '',
stripeFee: cells[6] ? (cells[6].textContent.trim() || '') : '',
amount: cells[7] ? (cells[7].textContent.trim() || '') : '',
processedBy: cells[8] ? (cells[8].textContent.trim() || '') : '',
status: cells[10] ? (cells[10].textContent.trim() || '') : '',
success: row.classList.contains('table-success'),
failed: row.classList.contains('table-danger'),
pending: row.classList.contains('table-info'),
refund: cells[10] && cells[10].textContent.includes('Refund'),
hasError: cells[9] && cells[9].querySelector('button.btn-outline-danger')
};
});
filteredPayments = [...allPayments];
updateResultCount();
console.log(`Single Payments List: Initialized ${allPayments.length} payments`);
}
function populatePaymentMethodFilter() {
const select = document.getElementById('paymentMethodFilter');
if (!select) return;
const methods = [...new Set(allPayments
.map(p => p.paymentMethod)
.filter(method => method && method !== '-')
)].sort();
// Clear existing options except "All Methods"
select.innerHTML = '<option value="all">All Methods</option>';
methods.forEach(method => {
const option = document.createElement('option');
option.value = method;
option.textContent = method;
select.appendChild(option);
});
}
function setupEventListeners() {
const searchInput = document.getElementById('searchInput');
const statusFilter = document.getElementById('statusFilter');
const paymentMethodFilter = document.getElementById('paymentMethodFilter');
const sortFilter = document.getElementById('sortFilter');
if (searchInput) searchInput.addEventListener('input', applyFilters);
if (statusFilter) statusFilter.addEventListener('change', applyFilters);
if (paymentMethodFilter) paymentMethodFilter.addEventListener('change', applyFilters);
if (sortFilter) sortFilter.addEventListener('change', applyFilters);
}
function applyFilters() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const statusFilter = document.getElementById('statusFilter').value;
const paymentMethodFilter = document.getElementById('paymentMethodFilter').value;
const sortFilter = document.getElementById('sortFilter').value;
// Filter payments
filteredPayments = allPayments.filter(payment => {
// Search filter
const searchMatch = !searchTerm ||
payment.splynxId.toLowerCase().includes(searchTerm) ||
payment.stripeCustomerId.toLowerCase().includes(searchTerm) ||
payment.paymentIntent.toLowerCase().includes(searchTerm) ||
payment.paymentId.toLowerCase().includes(searchTerm);
// Status filter
let statusMatch = true;
switch(statusFilter) {
case 'success':
statusMatch = payment.success;
break;
case 'failed':
statusMatch = payment.failed;
break;
case 'pending':
statusMatch = payment.pending;
break;
case 'refund':
statusMatch = payment.refund;
break;
case 'error':
statusMatch = payment.hasError;
break;
}
// Payment method filter
const methodMatch = paymentMethodFilter === 'all' ||
payment.paymentMethod === paymentMethodFilter;
return searchMatch && statusMatch && methodMatch;
});
// Sort payments
sortPayments(sortFilter);
// Update display
updateTable();
updateResultCount();
}
function sortPayments(sortBy) {
switch(sortBy) {
case 'date_desc':
// Already sorted by date desc in backend query
break;
case 'date_asc':
filteredPayments.reverse();
break;
case 'splynx_asc':
filteredPayments.sort((a, b) => parseInt(a.splynxId) - parseInt(b.splynxId));
break;
case 'splynx_desc':
filteredPayments.sort((a, b) => parseInt(b.splynxId) - parseInt(a.splynxId));
break;
case 'amount_asc':
filteredPayments.sort((a, b) => parseFloat(a.amount.replace(/[$,]/g, '')) - parseFloat(b.amount.replace(/[$,]/g, '')));
break;
case 'amount_desc':
filteredPayments.sort((a, b) => parseFloat(b.amount.replace(/[$,]/g, '')) - parseFloat(a.amount.replace(/[$,]/g, '')));
break;
case 'status':
filteredPayments.sort((a, b) => a.status.localeCompare(b.status));
break;
}
}
function updateTable() {
const tableBody = document.getElementById('paymentsTableBody');
// Hide all rows first
allPayments.forEach(payment => {
payment.element.style.display = 'none';
});
// Show filtered rows
filteredPayments.forEach(payment => {
payment.element.style.display = '';
tableBody.appendChild(payment.element); // Re-append to maintain sort order
});
}
function updateResultCount() {
const resultCount = document.getElementById('resultCount');
const filterResults = document.getElementById('filterResults');
if (!resultCount || !filterResults) return;
resultCount.textContent = filteredPayments.length;
if (filteredPayments.length === allPayments.length) {
filterResults.style.display = 'none';
} else {
filterResults.style.display = 'block';
}
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = 'all';
document.getElementById('paymentMethodFilter').value = 'all';
document.getElementById('sortFilter').value = 'date_desc';
applyFilters();
}
// Modal functionality (Bootstrap 5)
function showModal(modalId) {
console.log('Single Payments List: Opening modal ' + modalId);
const modalElement = document.getElementById(modalId);
if (!modalElement) {
console.error('Single Payments List: Modal element ' + modalId + ' not found!');
return;
}
const modal = new bootstrap.Modal(modalElement);
modal.show();
modalElement.addEventListener('shown.bs.modal', function() {
console.log('Single Payments List: Modal ' + modalId + ' is now visible');
}, { once: true });
}
function hideModal(modalId) {
const modalElement = document.getElementById(modalId);
if (!modalElement) return;
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
// Copy to clipboard functionality
function copyFormattedJSON(elementId) {
const element = document.getElementById(elementId);
if (!element) {
console.error('Single Payments List: Element ' + elementId + ' not found for copying');
return;
}
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"></i> Copied!';
button.classList.remove('btn-info', 'btn-primary', 'btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('btn-success');
// Restore original button class
if (elementId.includes('json-content')) {
button.classList.add('btn-info');
} else if (elementId.includes('followup-content')) {
button.classList.add('btn-primary');
} else if (elementId.includes('refund-content')) {
button.classList.add('btn-outline-secondary');
} else if (elementId.includes('error-content')) {
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"></i> Copied!';
button.classList.remove('btn-info', 'btn-primary', 'btn-outline-secondary');
button.classList.add('btn-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('btn-success');
if (elementId.includes('json-content')) {
button.classList.add('btn-info');
} else if (elementId.includes('followup-content')) {
button.classList.add('btn-primary');
} else if (elementId.includes('refund-content')) {
button.classList.add('btn-outline-secondary');
} else if (elementId.includes('error-content')) {
button.classList.add('btn-info');
}
}, 2000);
} catch (fallbackErr) {
console.error('Fallback copy failed: ', fallbackErr);
alert('Failed to copy to clipboard');
}
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 active" aria-current="page">Single Payments</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="h2 mb-1">Single Payments</h1>
<p class="text-light">Individual payment processing history</p>
</div>
<a class="btn btn-primary" href="{{ url_for('main.single_payment') }}">
<i class="fas fa-plus"></i> New Payment
</a>
</div>
<!-- Payment Details Table -->
<div class="card shadow">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 page-title">
<h2 class="h4 mb-0">Payment History</h2>
<div class="input-group" style="max-width: 400px;">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input class="form-control" type="text" id="searchInput" placeholder="Search Splynx ID, Customer ID, Payment Intent...">
</div>
</div>
<!-- Filter Controls -->
<div class="row g-3 mb-3">
<div class="col-md-3">
<label class="form-label form-label-sm">Filter by Status:</label>
<select class="form-select form-select-sm" id="statusFilter">
<option value="all">All Payments</option>
<option value="success">Successful Only</option>
<option value="failed">Failed Only</option>
<option value="pending">Pending Only</option>
<option value="refund">Refunds Only</option>
<option value="error">Has Errors</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label form-label-sm">Filter by Payment Method:</label>
<select class="form-select form-select-sm" id="paymentMethodFilter">
<option value="all">All Methods</option>
<!-- Options will be populated by JavaScript -->
</select>
</div>
<div class="col-md-3">
<label class="form-label form-label-sm">Sort by:</label>
<select class="form-select form-select-sm" id="sortFilter">
<option value="date_desc">Date (Newest First)</option>
<option value="date_asc">Date (Oldest First)</option>
<option value="splynx_asc">Splynx ID (Ascending)</option>
<option value="splynx_desc">Splynx ID (Descending)</option>
<option value="amount_desc">Amount (High to Low)</option>
<option value="amount_asc">Amount (Low to High)</option>
<option value="status">Status</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button class="btn btn-sm btn-info" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear Filters
</button>
</div>
</div>
<!-- Results Counter -->
<div class="alert alert-info" id="filterResults" style="display: none;">
<span id="resultCount">0</span> of {{ payments|length }} payments shown
</div>
{% if payments %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="paymentsTable">
<thead>
<tr>
<th>Payment ID</th>
<th>Date</th>
<th>Splynx ID</th>
<th>Stripe Customer</th>
<th>Payment Intent</th>
<th>Payment Method</th>
<th>Stripe Fee</th>
<th>Amount</th>
<th>Processed By</th>
<th>Data</th>
<th>Status</th>
</tr>
</thead>
<tbody id="paymentsTableBody">
{% for payment in payments %}
{% set row_class = '' %}
{% if payment.Refund == True %}
{% set row_class = 'table-secondary' %}
{% elif payment.Refund_FollowUp == True %}
{% set row_class = 'table-warning' %}
{% elif payment.Success == True %}
{% set row_class = 'table-success' %}
{% elif payment.Success == False and payment.PI_FollowUp %}
{% set row_class = 'table-warning' %}
{% elif payment.Success == False and payment.Error %}
{% set row_class = 'table-danger' %}
{% elif payment.Success == None %}
{% set row_class = 'table-info' %}
{% endif %}
<tr class="{{ row_class }}">
<td>
<a href="{{ url_for('main.single_payment_detail', payment_id=payment.id) }}"
class="fw-semibold text-primary">
#{{ payment.id }}
</a>
</td>
<td>
<span class="small">{{ payment.Created.strftime('%Y-%m-%d') }}</span><br>
<span class="small text-muted">{{ payment.Created.strftime('%H:%M:%S') }}</span>
</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>
<td>
{% if payment.Refund == True %}
<code class="small" style="background-color: #9370db; color: white; padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% elif payment.Refund_FollowUp == True %}
<code class="small bg-warning text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% elif payment.Success == True %}
<code class="small bg-success text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% elif payment.Success == False and payment.PI_FollowUp %}
<code class="small bg-warning text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% elif payment.Success == False and payment.Error %}
<code class="small bg-danger text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% elif payment.Success == None %}
<code class="small bg-info text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% else %}
<code class="small bg-light text-dark" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Stripe_Customer_ID or '-' }}</code>
{% endif %}
</td>
<td>
{% if payment.Payment_Intent %}
{% if payment.Refund == True %}
<code class="small" style="background-color: #9370db; color: white; padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% elif payment.Refund_FollowUp == True %}
<code class="small bg-warning text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% elif payment.Success == True %}
<code class="small bg-success text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% elif payment.Success == False and payment.PI_FollowUp %}
<code class="small bg-warning text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% elif payment.Success == False and payment.Error %}
<code class="small bg-danger text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% elif payment.Success == None %}
<code class="small bg-info text-white" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% else %}
<code class="small bg-light text-dark" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent }}</code>
{% endif %}
{% else %}
-
{% endif %}
</td>
<td>
{% if payment.Payment_Method %}
<span class="badge bg-info">{{ payment.Payment_Method }}</span>
{% else %}
-
{% endif %}
</td>
<td>
{% if payment.Fee_Stripe %}
{{ payment.Fee_Stripe | currency }}
{% else %}
-
{% endif %}
</td>
<td>
{% if payment.Payment_Amount %}
<strong>{{ payment.Payment_Amount | currency }}</strong>
{% else %}
-
{% endif %}
</td>
<td>
<span class="small">{{ payment.processed_by or 'Unknown' }}</span>
</td>
<td>
{% if payment.Error %}
{% set error_alert = payment | error_alert %}
{% if error_alert %}
<div class="error-alert-compact {{ error_alert.type }}" title="{{ error_alert.message }} - {{ error_alert.suggestion }}">
<i class="fas {{ error_alert.icon }}"></i>
<span>{{ error_alert.title }}</span>
</div>
{% endif %}
{% endif %}
<div class="btn-group btn-group-sm" role="group">
{% if payment.PI_JSON %}
<button class="btn btn-outline-info btn-sm" onclick="showModal('json-modal-{{ payment.id }}')">
<i class="fas fa-code"></i> JSON
</button>
{% endif %}
{% if payment.Refund_JSON %}
<button class="btn btn-outline-secondary btn-sm" style="border-color: #9370db; color: #9370db;" onclick="showModal('refund-modal-{{ payment.id }}')">
<i class="fas fa-undo"></i> Refund
</button>
{% endif %}
{% if payment.Error %}
<button class="btn btn-outline-danger btn-sm" onclick="showModal('error-modal-{{ payment.id }}')">
<i class="fas fa-exclamation-triangle"></i> Error
</button>
{% endif %}
</div>
</td>
<td>
{% if payment.Refund == True %}
<span class="status-badge refund">
<i class="fas fa-undo"></i>
Refund
</span>
{% elif payment.Refund_FollowUp == True %}
<span class="status-badge pending">
<i class="fas fa-clock"></i>
Refund Processing
</span>
{% elif payment.Success == True %}
<span class="status-badge success">
<i class="fas fa-check"></i>
Success
</span>
{% elif payment.Success == False and payment.PI_FollowUp %}
<span class="status-badge pending">
<i class="fas fa-clock"></i>
Pending
</span>
{% elif payment.Success == False %}
<span class="status-badge failed">
<i class="fas fa-times"></i>
Failed
</span>
{% else %}
<span class="status-badge pending">
<i class="fas fa-clock"></i>
Pending
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">
<p class="mb-0">No single payments found. <a href="{{ url_for('main.single_payment') }}" class="alert-link">Process your first payment</a>.</p>
</div>
{% endif %}
</div>
</div>
<!-- Modals for JSON/Error data -->
{% for payment in payments %}
<!-- PI_JSON Modal -->
{% if payment.PI_JSON %}
<div class="modal fade" id="json-modal-{{ payment.id }}" tabindex="-1" aria-labelledby="json-modal-label-{{ payment.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="json-modal-label-{{ payment.id }}">Payment JSON - Payment #{{ payment.id }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre><code class="language-json">{{ payment.PI_JSON | format_json }}</code></pre>
<button class="btn btn-sm btn-info" onclick="copyFormattedJSON('json-content-{{ payment.id }}')">
<i class="fas fa-copy"></i> Copy JSON
</button>
<div id="json-content-{{ payment.id }}" style="display: none;">{{ payment.PI_JSON | format_json }}</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Refund_JSON Modal -->
{% if payment.Refund_JSON %}
<div class="modal fade" id="refund-modal-{{ payment.id }}" tabindex="-1" aria-labelledby="refund-modal-label-{{ payment.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="refund-modal-label-{{ payment.id }}">Refund JSON - Payment #{{ payment.id }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre><code class="language-json">{{ payment.Refund_JSON | format_json }}</code></pre>
<button class="btn btn-sm btn-outline-secondary" style="border-color: #9370db; color: #9370db;" onclick="copyFormattedJSON('refund-content-{{ payment.id }}')">
<i class="fas fa-copy"></i> Copy JSON
</button>
<div id="refund-content-{{ payment.id }}" style="display: none;">{{ payment.Refund_JSON | format_json }}</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Error Modal -->
{% if payment.Error %}
{% set error_alert = payment | error_alert %}
<div class="modal fade" id="error-modal-{{ payment.id }}" tabindex="-1" aria-labelledby="error-modal-label-{{ payment.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="error-modal-label-{{ payment.id }}">
<i class="fas fa-exclamation-triangle"></i>
Payment Error - Payment #{{ payment.id }}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{% if error_alert %}
<div class="error-alert {{ error_alert.type }}">
<div class="error-alert-header">
<i class="fas {{ error_alert.icon }}"></i>
<span class="error-title">{{ error_alert.title }}</span>
</div>
<div class="error-alert-body">
<p class="error-message">{{ error_alert.message }}</p>
<p class="error-suggestion"><strong>Suggested Action:</strong> {{ error_alert.suggestion }}</p>
<details class="error-details">
<summary>View Technical Details</summary>
<pre>{{ error_alert.raw_error }}</pre>
</details>
</div>
</div>
{% else %}
<div class="alert alert-danger">
<h5 class="h6">Payment Error</h5>
<p>An error occurred during payment processing.</p>
<pre class="mt-3">{{ payment.Error }}</pre>
</div>
{% endif %}
<div class="mt-4">
<button class="btn btn-sm btn-info" onclick="copyFormattedJSON('error-content-{{ payment.id }}')">
<i class="fas fa-copy"></i> Copy Error Details
</button>
</div>
<div id="error-content-{{ payment.id }}" style="display: none;">{{ payment.Error }}</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endblock %}