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.
866 lines
31 KiB
866 lines
31 KiB
{% extends "base.html" %}
|
|
|
|
{% block title %}Single Payment - Plutus{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* Background styling */
|
|
body {
|
|
background-color: #3a3a3a !important;
|
|
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-attachment: fixed;
|
|
position: relative;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(58, 58, 58, 0.85);
|
|
z-index: -1;
|
|
}
|
|
|
|
/* (No page-level z-index; Bootstrap modals handle their own layering) */
|
|
|
|
/* Page title and breadcrumb styling */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
color: #faf8f0 !important;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.breadcrumb-item a {
|
|
color: #d4af37 !important;
|
|
}
|
|
|
|
.breadcrumb-item.active {
|
|
color: #faf8f0 !important;
|
|
}
|
|
|
|
.card, .box {
|
|
background-color: rgba(250, 248, 240, 0.98) !important;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.alert {
|
|
background-color: rgba(250, 248, 240, 0.98);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<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 Payment</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h2">Single Payment Processing</h1>
|
|
<p class="text-muted">Process individual customer payments through Stripe</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single Payment Form -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<!-- Step 1: Enter Splynx ID -->
|
|
<div id="step1" class="payment-step">
|
|
<h2 class="h4">
|
|
<i class="fas fa-search me-2"></i>
|
|
Customer Lookup
|
|
</h2>
|
|
|
|
<div class="field">
|
|
<label class="label" for="lookup_splynx_id">Splynx Customer ID</label>
|
|
<div class="control">
|
|
<input class="input" type="number" id="lookup_splynx_id" placeholder="Enter customer ID" required>
|
|
</div>
|
|
<p class="help">Enter the Splynx customer ID to fetch customer details</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading" class="text-center py-5 d-none">
|
|
<div class="spinner"></div>
|
|
<p class="mt-3">Fetching customer details...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="customerError" class="alert alert-danger d-none">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<span id="errorMessage">Customer not found or error occurred</span>
|
|
</div>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-primary" id="nextBtn" onclick="fetchCustomerDetails()">
|
|
<span class="icon"><i class="fas fa-arrow-right"></i></span>
|
|
<span>Next</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Select Invoices (NEW) -->
|
|
<div id="step2" class="payment-step d-none">
|
|
<h2 class="h4">
|
|
<i class="fas fa-file-invoice me-2"></i>
|
|
Select Invoices to Pay
|
|
</h2>
|
|
|
|
<!-- Loading State -->
|
|
<div id="invoiceLoading" class="text-center py-5 d-none">
|
|
<div class="spinner"></div>
|
|
<p class="mt-3">Fetching invoices...</p>
|
|
</div>
|
|
|
|
<!-- No Invoices State -->
|
|
<div id="noInvoices" class="alert alert-info d-none">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
No unpaid invoices found. You can still process a payment.
|
|
</div>
|
|
|
|
<!-- Invoices List -->
|
|
<div id="invoicesList" class="d-none">
|
|
<div class="alert alert-info mb-4">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
Select which invoices this payment should be applied to. You can select multiple invoices or skip to process a general payment.
|
|
</div>
|
|
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="selectAllInvoices" onchange="toggleAllInvoices()">
|
|
<label class="form-check-label" for="selectAllInvoices">
|
|
<strong>Select All Invoices</strong>
|
|
</label>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div id="invoicesContainer">
|
|
<!-- Invoice checkboxes populated by JavaScript -->
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<label class="form-label">Total Selected Amount</label>
|
|
<p class="h4" id="selectedInvoicesTotal">$0.00</p>
|
|
</div>
|
|
</div>
|
|
</div> <!-- /.card -->
|
|
</div>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-light" onclick="goBackToStep1()">
|
|
<span class="icon"><i class="fas fa-arrow-left"></i></span>
|
|
<span>Back</span>
|
|
</button>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-primary" onclick="goToStep3()">
|
|
<span class="icon"><i class="fas fa-arrow-right"></i></span>
|
|
<span>Continue to Payment</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Confirm Customer & Enter Amount -->
|
|
<div id="step3" class="payment-step d-none">
|
|
<h2 class="h4">
|
|
<i class="fas fa-user-check me-2"></i>
|
|
Confirm Customer & Payment Details
|
|
</h2>
|
|
|
|
<div class="card bg-light mb-5">
|
|
<div class="card-body">
|
|
<h3 class="h5">Customer Information</h3>
|
|
<div id="customerDetails">
|
|
<!-- Customer details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div> <!-- /.card -->
|
|
|
|
<form id="paymentForm">
|
|
<input type="hidden" id="confirmed_splynx_id" name="splynx_id">
|
|
|
|
<div class="field">
|
|
<label class="label" for="payment_amount">Payment Amount (AUD)</label>
|
|
<div class="control has-icons-left">
|
|
<input class="input is-large" type="number" step="0.01" min="0.01" max="10000"
|
|
id="payment_amount" name="amount" placeholder="0.00" required>
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-dollar-sign"></i>
|
|
</span>
|
|
</div>
|
|
<p class="help">Enter the amount to charge (maximum $10,000)</p>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label class="label" for="payment_method_select">Payment Method</label>
|
|
<div class="control has-icons-left" id="payment_method_container">
|
|
<div class="select is-fullwidth is-loading" id="payment_method_loading">
|
|
<select disabled>
|
|
<option>Loading payment methods...</option>
|
|
</select>
|
|
</div>
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-credit-card"></i>
|
|
</span>
|
|
</div>
|
|
<div id="payment_method_error" class="alert alert-danger d-none mt-2">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<span>Unable to load payment methods. Customer may not have any valid payment methods.</span>
|
|
</div>
|
|
<p class="form-text">Select which payment method to use for this payment</p>
|
|
</div>
|
|
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
This payment will be processed immediately using the selected payment method.
|
|
</div>
|
|
</form>
|
|
|
|
<div class="field is-grouped">
|
|
<div class="control">
|
|
<button class="button is-light" id="backBtn" onclick="goBackToStep2()">
|
|
<span class="icon"><i class="fas fa-arrow-left"></i></span>
|
|
<span>Back</span>
|
|
</button>
|
|
</div>
|
|
<div class="control">
|
|
<button class="button is-warning" id="processBtn" onclick="showConfirmationModal()">
|
|
<span class="icon"><i class="fas fa-credit-card"></i></span>
|
|
<span>Process Payment</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div> <!-- /.card -->
|
|
|
|
<!-- Payment Confirmation Modal -->
|
|
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-labelledby="confirmationModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-warning">
|
|
<h5 class="modal-title" id="confirmationModalLabel">Confirm Payment Processing</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p><strong>Customer:</strong> <span id="confirmCustomerName"></span></p>
|
|
<p><strong>Amount:</strong> <span id="confirmAmount"></span></p>
|
|
<p><strong>Payment Method:</strong> <span id="confirmPaymentMethod"></span></p>
|
|
<p class="mb-0">Are you sure you want to process this payment?</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-warning" id="confirmPaymentBtn" onclick="processPayment()">
|
|
<i class="fas fa-credit-card me-2"></i>Confirm Payment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Success Modal -->
|
|
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-success text-white">
|
|
<h5 class="modal-title" id="successModalLabel">
|
|
<i class="fas fa-check-circle me-2"></i>Payment Successful
|
|
</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-check-circle fa-3x text-success mb-4"></i>
|
|
<h3 class="h4">Payment Processed Successfully!</h3>
|
|
<div id="successMessage" class="mt-3">
|
|
<!-- Success details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center">
|
|
<button type="button" class="btn btn-primary" onclick="closeSuccessModal()">
|
|
<i class="fas fa-check me-2"></i>Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fee Update Modal (Orange) -->
|
|
<div class="modal fade" id="feeUpdateModal" tabindex="-1" aria-labelledby="feeUpdateModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-warning">
|
|
<h5 class="modal-title" id="feeUpdateModalLabel">
|
|
<i class="fas fa-clock me-2"></i>Direct Debit Processing
|
|
</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-clock fa-3x text-warning mb-4"></i>
|
|
<h3 class="h4">Direct Debit is still being processed</h3>
|
|
<div class="mt-3">
|
|
<p>Your Direct Debit payment is currently being processed by the bank. This can take a few minutes to complete.</p>
|
|
<p><strong>Please check back later or click the button below to view payment details.</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center">
|
|
<button type="button" class="btn btn-warning" id="viewPaymentDetailsBtn" onclick="viewPaymentDetails()">
|
|
<i class="fas fa-eye me-2"></i>View Payment Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Modal -->
|
|
<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-danger text-white">
|
|
<h5 class="modal-title" id="errorModalLabel">
|
|
<i class="fas fa-exclamation-circle me-2"></i>Payment Failed
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center py-4">
|
|
<i class="fas fa-exclamation-circle fa-3x text-danger mb-4"></i>
|
|
<h3 class="h4">Payment Processing Failed</h3>
|
|
<div id="errorDetails" class="mt-3">
|
|
<!-- Error details will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer justify-content-center">
|
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Loading spinner */
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid rgba(212, 175, 55, 0.3);
|
|
border-radius: 50%;
|
|
border-top-color: var(--plutus-gold);
|
|
animation: spin 1s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Step transitions */
|
|
.payment-step {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.payment-step.d-none {
|
|
display: none;
|
|
}
|
|
|
|
/* Enhanced form styling */
|
|
.input.is-large {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let currentCustomerData = null;
|
|
let currentPaymentId = null;
|
|
let allInvoices = [];
|
|
let selectedInvoices = [];
|
|
|
|
function fetchCustomerDetails() {
|
|
const splynxIdElement = document.getElementById('lookup_splynx_id');
|
|
const splynxId = splynxIdElement ? splynxIdElement.value : '';
|
|
|
|
// Clear previous errors
|
|
document.getElementById('customerError').classList.add('d-none');
|
|
|
|
if (!splynxId || splynxId.trim() === '' || splynxId.trim() === '0') {
|
|
showError('Please enter a valid Splynx Customer ID');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
document.getElementById('loading').classList.remove('d-none');
|
|
document.getElementById('nextBtn').disabled = true;
|
|
|
|
const apiUrl = `/api/splynx/${splynxId.trim()}`;
|
|
|
|
// Make API call
|
|
fetch(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
// Hide loading
|
|
document.getElementById('loading').classList.add('d-none');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
|
|
if (data && data.id) {
|
|
currentCustomerData = data;
|
|
displayCustomerDetails(data);
|
|
goToStep2();
|
|
} else {
|
|
showError('Customer not found or invalid data received');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching customer:', error);
|
|
document.getElementById('loading').classList.add('d-none');
|
|
document.getElementById('nextBtn').disabled = false;
|
|
showError(`Failed to fetch customer details: ${error.message}`);
|
|
});
|
|
}
|
|
|
|
function displayCustomerDetails(customer) {
|
|
const detailsHtml = `
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<strong>Name:</strong><br>
|
|
<span>${customer.name || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Customer ID:</strong><br>
|
|
<span class="badge bg-info">${customer.id}</span>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Status:</strong><br>
|
|
${customer.status === 'active'
|
|
? '<span class="badge bg-success">Active</span>'
|
|
: `<span class="badge bg-warning">${customer.status || 'Unknown'}</span>`
|
|
}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Email:</strong><br>
|
|
<span>${customer.email || 'N/A'}</span>
|
|
</div>
|
|
<div class="col-12">
|
|
<strong>Address:</strong><br>
|
|
<span>${customer.street_1 || ''} ${customer.street_2 || ''}<br>
|
|
${customer.city || ''} ${customer.zip_code || ''}</span>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<strong>Phone:</strong><br>
|
|
<span>${customer.phone || 'N/A'}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('customerDetails').innerHTML = detailsHtml;
|
|
document.getElementById('confirmed_splynx_id').value = customer.id;
|
|
|
|
// Fetch payment methods for this customer
|
|
fetchPaymentMethods(customer.id);
|
|
|
|
// Fetch invoices for this customer
|
|
fetchInvoices(customer.id);
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('errorMessage').textContent = message;
|
|
document.getElementById('customerError').classList.remove('d-none');
|
|
}
|
|
|
|
function fetchPaymentMethods(splynxId) {
|
|
// Get the Stripe customer ID from MySQL first
|
|
fetch(`/api/stripe-customer-id/${splynxId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && data.stripe_customer_id) {
|
|
// Now fetch payment methods for this Stripe customer
|
|
return fetch(`/api/stripe-payment-methods/${data.stripe_customer_id}`);
|
|
} else {
|
|
throw new Error('Customer does not have a Stripe customer ID');
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && data.payment_methods) {
|
|
displayPaymentMethods(data.payment_methods);
|
|
} else {
|
|
showPaymentMethodError();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching payment methods:', error);
|
|
showPaymentMethodError();
|
|
});
|
|
}
|
|
|
|
function displayPaymentMethods(paymentMethods) {
|
|
const container = document.getElementById('payment_method_container');
|
|
|
|
if (!paymentMethods || paymentMethods.length === 0) {
|
|
showPaymentMethodError();
|
|
return;
|
|
}
|
|
|
|
// Create the select element
|
|
const selectHtml = `
|
|
<div class="select is-fullwidth">
|
|
<select id="payment_method_select" name="payment_method" required>
|
|
<option value="">Select a payment method</option>
|
|
${paymentMethods.map(pm => {
|
|
let displayText = '';
|
|
if (pm.type === 'card' && pm.card) {
|
|
const brand = pm.card.brand || 'Card';
|
|
const last4 = pm.card.last4 || '****';
|
|
displayText = `${brand.toUpperCase()} ending in ${last4}`;
|
|
} else if (pm.type === 'au_becs_debit' && pm.au_becs_debit) {
|
|
const last4 = pm.au_becs_debit.last4 || '****';
|
|
displayText = `AU Bank Account ending in ${last4}`;
|
|
} else {
|
|
displayText = pm.type.toUpperCase();
|
|
}
|
|
return `<option value="${pm.id}">${displayText}</option>`;
|
|
}).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = selectHtml + `
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-credit-card"></i>
|
|
</span>
|
|
`;
|
|
|
|
// Hide any error messages
|
|
document.getElementById('payment_method_error').classList.add('d-none');
|
|
}
|
|
|
|
function showPaymentMethodError() {
|
|
const container = document.getElementById('payment_method_container');
|
|
|
|
// Show a disabled select with error message
|
|
container.innerHTML = `
|
|
<div class="select is-fullwidth">
|
|
<select disabled>
|
|
<option>No payment methods available</option>
|
|
</select>
|
|
</div>
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</span>
|
|
`;
|
|
|
|
// Show error notification
|
|
document.getElementById('payment_method_error').classList.remove('d-none');
|
|
}
|
|
|
|
function goToStep2() {
|
|
// Hide step 1, show step 2 (invoice selection)
|
|
document.getElementById('step1').classList.add('d-none');
|
|
document.getElementById('step2').classList.remove('d-none');
|
|
}
|
|
|
|
function goBackToStep1() {
|
|
// Show step 1, hide step 2
|
|
document.getElementById('step1').classList.remove('d-none');
|
|
document.getElementById('step2').classList.add('d-none');
|
|
|
|
// Clear any errors
|
|
document.getElementById('customerError').classList.add('d-none');
|
|
document.getElementById('payment_method_error').classList.add('d-none');
|
|
|
|
// Reset payment method selector to loading state
|
|
const container = document.getElementById('payment_method_container');
|
|
container.innerHTML = `
|
|
<div class="select is-fullwidth is-loading" id="payment_method_loading">
|
|
<select disabled>
|
|
<option>Loading payment methods...</option>
|
|
</select>
|
|
</div>
|
|
<span class="icon is-small is-left">
|
|
<i class="fas fa-credit-card"></i>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
function goBackToStep2() {
|
|
// Show step 2, hide step 3
|
|
document.getElementById('step3').classList.add('d-none');
|
|
document.getElementById('step2').classList.remove('d-none');
|
|
}
|
|
|
|
function fetchInvoices(splynxId) {
|
|
document.getElementById('invoiceLoading').classList.remove('d-none');
|
|
|
|
fetch(`/api/splynx/invoices/${splynxId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('invoiceLoading').classList.add('d-none');
|
|
|
|
if (data.success && data.invoices && data.invoices.length > 0) {
|
|
allInvoices = data.invoices;
|
|
displayInvoices(data.invoices);
|
|
} else {
|
|
document.getElementById('noInvoices').classList.remove('d-none');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching invoices:', error);
|
|
document.getElementById('invoiceLoading').classList.add('d-none');
|
|
document.getElementById('noInvoices').classList.remove('d-none');
|
|
});
|
|
}
|
|
|
|
function displayInvoices(invoices) {
|
|
const container = document.getElementById('invoicesContainer');
|
|
|
|
let html = '';
|
|
invoices.forEach(invoice => {
|
|
html += `
|
|
<div class="form-check">
|
|
<input class="form-check-input invoice-checkbox" type="checkbox"
|
|
value="${invoice.id}"
|
|
data-amount="${invoice.total}"
|
|
onchange="updateSelectedTotal()"
|
|
id="invoice-${invoice.id}">
|
|
<label class="form-check-label" for="invoice-${invoice.id}">
|
|
<strong>${invoice.number}</strong> - ${invoice.date} -
|
|
<span class="fw-bold">$${invoice.total.toFixed(2)}</span>
|
|
<br>
|
|
<span class="text-muted small">${invoice.description}</span>
|
|
</label>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
document.getElementById('invoicesList').classList.remove('d-none');
|
|
}
|
|
|
|
function toggleAllInvoices() {
|
|
const selectAll = document.getElementById('selectAllInvoices').checked;
|
|
document.querySelectorAll('.invoice-checkbox').forEach(cb => cb.checked = selectAll);
|
|
updateSelectedTotal();
|
|
}
|
|
|
|
function updateSelectedTotal() {
|
|
const checkboxes = document.querySelectorAll('.invoice-checkbox:checked');
|
|
selectedInvoices = [];
|
|
let total = 0;
|
|
|
|
checkboxes.forEach(cb => {
|
|
selectedInvoices.push(cb.value);
|
|
total += parseFloat(cb.dataset.amount);
|
|
});
|
|
|
|
document.getElementById('selectedInvoicesTotal').textContent = `$${total.toFixed(2)}`;
|
|
}
|
|
|
|
function goToStep3() {
|
|
// Hide step 2, show step 3
|
|
document.getElementById('step2').classList.add('d-none');
|
|
document.getElementById('step3').classList.remove('d-none');
|
|
|
|
// Pre-fill amount with selected invoice total if any selected
|
|
if (selectedInvoices.length > 0) {
|
|
const total = Array.from(document.querySelectorAll('.invoice-checkbox:checked'))
|
|
.reduce((sum, cb) => sum + parseFloat(cb.dataset.amount), 0);
|
|
document.getElementById('payment_amount').value = total.toFixed(2);
|
|
}
|
|
|
|
// Focus on amount input
|
|
document.getElementById('payment_amount').focus();
|
|
}
|
|
|
|
function showConfirmationModal() {
|
|
const amount = document.getElementById('payment_amount').value;
|
|
const paymentMethodSelect = document.getElementById('payment_method_select');
|
|
|
|
if (!amount || parseFloat(amount) <= 0) {
|
|
alert('Please enter a valid payment amount');
|
|
return;
|
|
}
|
|
|
|
if (!paymentMethodSelect || !paymentMethodSelect.value) {
|
|
alert('Please select a payment method');
|
|
return;
|
|
}
|
|
|
|
if (!currentCustomerData) {
|
|
alert('Customer data not found. Please restart the process.');
|
|
return;
|
|
}
|
|
|
|
// Update confirmation modal content
|
|
document.getElementById('confirmCustomerName').textContent = currentCustomerData.name || 'Unknown';
|
|
document.getElementById('confirmAmount').textContent = `$${parseFloat(amount).toFixed(2)}`;
|
|
document.getElementById('confirmPaymentMethod').textContent = paymentMethodSelect.options[paymentMethodSelect.selectedIndex].text;
|
|
|
|
// Show modal using Bootstrap Modal API
|
|
const modalEl = document.getElementById('confirmationModal');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
}
|
|
|
|
function processPayment() {
|
|
const form = document.getElementById('paymentForm');
|
|
const formData = new FormData(form);
|
|
|
|
// Add selected invoice IDs
|
|
formData.append('invoice_ids', selectedInvoices.join(','));
|
|
|
|
// Disable confirm button and show loading
|
|
const confirmBtn = document.getElementById('confirmPaymentBtn');
|
|
const originalText = confirmBtn.innerHTML;
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<span class="icon"><i class="fas fa-spinner fa-spin"></i></span><span>Processing...</span>';
|
|
|
|
// Submit the payment
|
|
fetch('/single-payment/process', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => {
|
|
return response.json().then(data => {
|
|
return { status: response.status, data: data };
|
|
});
|
|
})
|
|
.then(result => {
|
|
// Hide confirmation modal
|
|
hideModal('confirmationModal');
|
|
|
|
const { status, data } = result;
|
|
|
|
// Check if payment was successful
|
|
if (status === 200 && data.success && data.payment_success) {
|
|
showSuccessModal(data);
|
|
} else if (status === 422 && data.fee_update) {
|
|
// Direct Debit needs fee update - show orange modal
|
|
showFeeUpdateModal(data);
|
|
} else {
|
|
// Payment failed or had an error - show the specific error
|
|
let errorMessage;
|
|
|
|
if (status === 422) {
|
|
// Payment processing failed (business logic error)
|
|
errorMessage = `Payment Failed: ${data.stripe_error || data.error || 'Unknown error'}`;
|
|
} else if (status >= 400) {
|
|
// Other HTTP errors
|
|
errorMessage = data.error || 'Payment processing failed';
|
|
} else {
|
|
// Unexpected status
|
|
errorMessage = 'Payment processing failed. Please try again.';
|
|
}
|
|
|
|
showErrorModal(errorMessage);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error processing payment:', error);
|
|
hideModal('confirmationModal');
|
|
showErrorModal('Payment processing failed. Please try again.');
|
|
})
|
|
.finally(() => {
|
|
// Re-enable button
|
|
confirmBtn.disabled = false;
|
|
confirmBtn.innerHTML = originalText;
|
|
});
|
|
}
|
|
|
|
function showSuccessModal(data) {
|
|
const successHtml = `
|
|
<p><strong>Payment ID:</strong> ${data.payment_id}</p>
|
|
<p><strong>Payment Intent:</strong> ${data.payment_intent || 'N/A'}</p>
|
|
<p><strong>Amount:</strong> $${parseFloat(data.amount).toFixed(2)}</p>
|
|
<p><strong>Customer:</strong> ${data.customer_name}</p>
|
|
`;
|
|
|
|
document.getElementById('successMessage').innerHTML = successHtml;
|
|
|
|
// Show modal using Bootstrap Modal API
|
|
const modalEl = document.getElementById('successModal');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
}
|
|
|
|
function showErrorModal(errorMessage) {
|
|
document.getElementById('errorDetails').innerHTML = `<p>${errorMessage}</p>`;
|
|
|
|
// Show modal using Bootstrap Modal API
|
|
const modalEl = document.getElementById('errorModal');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
}
|
|
|
|
function hideModal(modalId) {
|
|
const el = document.getElementById(modalId);
|
|
if (!el) return;
|
|
const modal = bootstrap.Modal.getInstance(el) || bootstrap.Modal.getOrCreateInstance(el);
|
|
modal.hide();
|
|
}
|
|
|
|
function showFeeUpdateModal(data) {
|
|
currentPaymentId = data.payment_id;
|
|
|
|
// Show modal using Bootstrap Modal API
|
|
const modalEl = document.getElementById('feeUpdateModal');
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
}
|
|
|
|
function viewPaymentDetails() {
|
|
if (currentPaymentId) {
|
|
// Redirect to the single payment detail page
|
|
window.location.href = `/single-payment/detail/${currentPaymentId}`;
|
|
}
|
|
}
|
|
|
|
function closeSuccessModal() {
|
|
hideModal('successModal');
|
|
// Reset form to step 1
|
|
goBackToStep1();
|
|
document.getElementById('lookup_splynx_id').value = '';
|
|
document.getElementById('payment_amount').value = '';
|
|
currentCustomerData = null;
|
|
allInvoices = [];
|
|
selectedInvoices = [];
|
|
}
|
|
|
|
// Bootstrap modals handle Escape key automatically, no need for custom handler
|
|
|
|
// Enter key navigation
|
|
document.getElementById('lookup_splynx_id').addEventListener('keypress', function(event) {
|
|
if (event.key === 'Enter') {
|
|
fetchCustomerDetails();
|
|
}
|
|
});
|
|
|
|
document.getElementById('payment_amount').addEventListener('keypress', function(event) {
|
|
if (event.key === 'Enter') {
|
|
showConfirmationModal();
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|