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.
944 lines
38 KiB
944 lines
38 KiB
{% extends "base.html" %}
|
|
|
|
{% block title %}Batch #{{ batch.id }} - Plutus{% endblock %}
|
|
|
|
{% block head %}
|
|
|
|
<style>
|
|
/* Background styling for Batch Detail page */
|
|
body {
|
|
background-color: #3a3a3a !important;
|
|
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-attachment: fixed;
|
|
position: relative;
|
|
}
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(58, 58, 58, 0.85);
|
|
z-index: -1;
|
|
}
|
|
|
|
/* Page title 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;
|
|
}
|
|
|
|
/* Ensure content is visible on top of background */
|
|
.container-fluid, .container {
|
|
position: relative;
|
|
/* z-index removed to avoid creating a stacking context that traps Bootstrap modals */
|
|
}
|
|
/* Ensure navbar and footer are above background */
|
|
.navbar, .footer, main {
|
|
position: relative;
|
|
/* z-index removed to avoid creating a stacking context that traps Bootstrap modals */
|
|
}
|
|
|
|
/* --- FIX: allow Bootstrap modal to escape ancestor z-index --- */
|
|
.container-fluid, .container,
|
|
.navbar, .footer, main {
|
|
z-index: auto !important; /* explicitly reset any inherited/cached z-index */
|
|
}
|
|
|
|
/* Enhanced card visibility */
|
|
.card {
|
|
background-color: rgba(250, 248, 240, 0.98) !important;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Table container with enhanced visibility */
|
|
.table-responsive {
|
|
background-color: rgba(250, 248, 240, 0.98);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
border: 2px solid rgba(212, 175, 55, 0.5);
|
|
}
|
|
|
|
/* Alert styling for better visibility */
|
|
.alert {
|
|
background-color: rgba(250, 248, 240, 0.98);
|
|
border: 2px solid rgba(212, 175, 55, 0.5);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Modal styling to match Hades theme */
|
|
/* (No page-specific modal z-index; 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('Batch Detail: DOM loaded, initializing payment filtering and modals');
|
|
initializePayments();
|
|
populatePaymentMethodFilter();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function initializePayments() {
|
|
const tableBody = document.getElementById('paymentsTableBody');
|
|
if (!tableBody) {
|
|
console.error('Batch Detail: 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() || '') : '',
|
|
splynxId: cells[1] ? (cells[1].textContent.trim() || '') : '',
|
|
stripeCustomerId: cells[2] ? (cells[2].textContent.trim() || '') : '',
|
|
paymentIntent: cells[3] ? (cells[3].textContent.trim() || '') : '',
|
|
followUp: cells[4] ? (cells[4].textContent.trim() || '') : '',
|
|
lastCheck: cells[5] ? (cells[5].textContent.trim() || '') : '',
|
|
paymentMethod: cells[6] ? (cells[6].textContent.trim() || '') : '',
|
|
stripeFee: cells[7] ? (cells[7].textContent.trim() || '') : '',
|
|
amount: 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'),
|
|
followUpRequired: row.classList.contains('table-warning'),
|
|
refund: cells[10] && cells[10].textContent.includes('Refund'),
|
|
hasError: cells[9] && cells[9].querySelector('button.btn-outline-danger')
|
|
};
|
|
});
|
|
|
|
filteredPayments = [...allPayments];
|
|
updateResultCount();
|
|
console.log(`Batch Detail: 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.paymentId.toLowerCase().includes(searchTerm) ||
|
|
payment.splynxId.toLowerCase().includes(searchTerm) ||
|
|
payment.stripeCustomerId.toLowerCase().includes(searchTerm) ||
|
|
payment.paymentIntent.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 'followup':
|
|
statusMatch = payment.followUpRequired;
|
|
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 '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 = 'splynx_asc';
|
|
applyFilters();
|
|
}
|
|
|
|
// Modal functionality (Bootstrap 5)
|
|
function showModal(modalId) {
|
|
console.log('Batch Detail: Opening modal ' + modalId);
|
|
const modalElement = document.getElementById(modalId);
|
|
if (!modalElement) {
|
|
console.error('Batch Detail: Modal element ' + modalId + ' not found!');
|
|
return;
|
|
}
|
|
|
|
const modal = new bootstrap.Modal(modalElement);
|
|
modal.show();
|
|
|
|
modalElement.addEventListener('shown.bs.modal', function() {
|
|
console.log('Batch Detail: 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('Batch Detail: 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"><a href="{{ url_for('main.batch_list') }}">Payment Batches</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">Batch #{{ batch.id }}</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
|
<div>
|
|
<h1 class="h2 mb-1">Payment Batch #{{ batch.id }}</h1>
|
|
<p class="text-light">Created: {{ batch.Created.strftime('%Y-%m-%d %H:%M:%S') if batch.Created else 'Unknown' }}</p>
|
|
</div>
|
|
<a class="btn btn-light" href="{{ url_for('main.batch_list') }}">
|
|
<i class="fas fa-arrow-left"></i> Back to Batches
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Summary Statistics -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<div class="card shadow">
|
|
<div class="card-body">
|
|
<div class="row text-center">
|
|
<div class="col-md-4">
|
|
<p class="text-muted text-uppercase small mb-1">Total Payments</p>
|
|
<p class="h3 mb-0">{{ summary.payment_count or 0 }}</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<p class="text-muted text-uppercase small mb-1">Payment Amount</p>
|
|
<p class="h3 mb-0">{{ summary.total_amount | currency }}</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<p class="text-muted text-uppercase small mb-1">Stripe Fees</p>
|
|
<p class="h3 mb-0">{{ summary.total_fees | currency }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card shadow">
|
|
<div class="card-body text-center">
|
|
<p class="text-muted text-uppercase small mb-1">Successful</p>
|
|
<p class="h3 mb-0 text-success">{{ summary.successful_count or 0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow">
|
|
<div class="card-body text-center">
|
|
<p class="text-muted text-uppercase small mb-1">Failed</p>
|
|
<p class="h3 mb-0 text-danger">{{ summary.failed_count or 0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow">
|
|
<div class="card-body text-center">
|
|
<p class="text-muted text-uppercase small mb-1">Errors</p>
|
|
<p class="h3 mb-0 text-warning">{{ summary.error_count or 0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card shadow">
|
|
<div class="card-body text-center">
|
|
<p class="text-muted text-uppercase small mb-1">Success Rate</p>
|
|
{% if summary.payment_count and summary.payment_count > 0 %}
|
|
{% set success_rate = (summary.successful_count or 0) / summary.payment_count * 100 %}
|
|
<p class="h3 mb-0 {% if success_rate >= 90 %}text-success{% elif success_rate >= 70 %}text-warning{% else %}text-danger{% endif %}">
|
|
{{ "%.1f"|format(success_rate) }}%
|
|
</p>
|
|
{% else %}
|
|
<p class="h3 mb-0">0%</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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 Details</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="followup">Follow Up Required</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="splynx_asc">Splynx ID (Ascending)</option>
|
|
<option value="splynx_desc">Splynx ID (Descending)</option>
|
|
<option value="amount_asc">Amount (Low to High)</option>
|
|
<option value="amount_desc">Amount (High to Low)</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>Splynx ID</th>
|
|
<th>Stripe Customer</th>
|
|
<th>Payment Intent</th>
|
|
<th>Follow Up</th>
|
|
<th>Last Check</th>
|
|
<th>Payment Method</th>
|
|
<th>Stripe Fee</th>
|
|
<th>Amount</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.payment_detail', payment_id=payment.id) }}"
|
|
class="fw-semibold text-primary">
|
|
#{{ payment.id }}
|
|
</a>
|
|
</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-dark" 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.Refund == True %}
|
|
<code class="small" style="background-color: #9370db; color: white; padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent 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.Payment_Intent or '-' }}</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 or '-' }}</code>
|
|
{% elif payment.Success == False and payment.PI_FollowUp %}
|
|
<code class="small bg-warning text-dark" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent 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.Payment_Intent or '-' }}</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 or '-' }}</code>
|
|
{% else %}
|
|
<code class="small bg-light text-dark" style="padding: 0.2rem 0.4rem; border-radius: 0.25rem;">{{ payment.Payment_Intent or '-' }}</code>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if payment.PI_FollowUp %}
|
|
<span class="badge bg-warning">Follow Up</span>
|
|
{% else %}
|
|
<span class="badge bg-light text-dark">No</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{{ payment.PI_Last_Check.strftime('%Y-%m-%d %H:%M') if payment.PI_Last_Check else '-' }}
|
|
</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>
|
|
{% 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.PI_FollowUp_JSON %}
|
|
<button class="btn btn-outline-primary btn-sm" onclick="showModal('followup-modal-{{ payment.id }}')">
|
|
<i class="fas fa-redo"></i> Follow Up
|
|
</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 payments found in this batch.</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 Intent 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 %}
|
|
|
|
<!-- PI_FollowUp_JSON Modal -->
|
|
{% if payment.PI_FollowUp_JSON %}
|
|
<div class="modal fade" id="followup-modal-{{ payment.id }}" tabindex="-1" aria-labelledby="followup-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="followup-modal-label-{{ payment.id }}">Follow Up 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_FollowUp_JSON | format_json }}</code></pre>
|
|
<button class="btn btn-sm btn-primary" onclick="copyFormattedJSON('followup-content-{{ payment.id }}')">
|
|
<i class="fas fa-copy"></i> Copy JSON
|
|
</button>
|
|
<div id="followup-content-{{ payment.id }}" style="display: none;">{{ payment.PI_FollowUp_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 %}
|