Files
railsgoat/app/views/pay/index.html.erb
T
Ken Johnson 24cb70edca Fix DataTables initialization error on pay page
Resolves "Cannot set properties of undefined (setting '_DT_CellIndex')"
error by modernizing DataTables API usage and handling Turbolinks properly.

Changes:
- Update to modern DataTables API (capital D DataTable() vs lowercase)
- Add check for existing DataTable before initialization
- Properly destroy and recreate DataTable on Turbolinks page loads
- Replace deprecated fnClearTable() with table.clear()
- Replace deprecated fnAddData() with table.row.add() + table.draw()
- Create unified initializePage() function for both ready and turbolinks:load
- Add autoWidth, searching, and ordering options to DataTable config

The DataTable now initializes cleanly without errors and handles
Turbolinks navigation properly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-07 03:13:19 -05:00

512 lines
18 KiB
Plaintext

<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="bi bi-bank text-primary"></i> Direct Deposit & Pay
</h2>
<p class="text-muted">Manage your direct deposit accounts and payment settings</p>
</div>
</div>
<!-- Alert Messages -->
<div class="row">
<div class="col-12">
<div id="success" style="display: none;" class="alert alert-success alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Success!</h5>
<p class="mb-0">Information successfully updated.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="failure" style="display: none;" class="alert alert-danger alert-dismissible fade show" role="alert">
<div class="d-flex align-items-center">
<i class="bi bi-exclamation-triangle-fill me-2" style="font-size: 1.5rem;"></i>
<div>
<h5 class="alert-heading mb-1">Error!</h5>
<p class="mb-0">Failed to update. Please try again.</p>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<div class="row g-3">
<!-- Left Column - Forms -->
<div class="col-lg-4">
<!-- Add Direct Deposit Form -->
<div class="card shadow-sm mb-3" style="border-left: 4px solid var(--rg-success);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(6, 214, 160, 0.05), rgba(30, 130, 94, 0.05));">
<h4 class="mb-0">
<i class="bi bi-plus-circle text-success"></i> Add Direct Deposit
</h4>
<p class="text-muted mb-0 small mt-1">Set up a new account</p>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "bank_info_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-hash text-success me-2"></i>Bank Account Number
</label>
<div class="input-group input-group-lg">
<%= text_field_tag :bank_account_num, params[:bank_account_num], {
placeholder: "Enter account number",
class: "form-control"
} %>
<span class="input-group-text bg-white">
<i class="bi bi-bank2"></i>
</span>
</div>
<small class="text-muted">Your bank account number</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-sign-turn-right text-success me-2"></i>Bank Routing Number
</label>
<div class="input-group input-group-lg">
<%= text_field_tag :bank_routing_num, params[:bank_routing_num], {
placeholder: "Enter routing number",
class: "form-control"
} %>
<span class="input-group-text bg-white">
<i class="bi bi-diagram-3"></i>
</span>
</div>
<small class="text-muted">9-digit routing number</small>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-percent text-success me-2"></i>Percentage of Deposit
</label>
<div class="input-group input-group-lg">
<%= text_field_tag :dd_percent, params[:dd_percent], {
placeholder: "e.g., 100",
class: "form-control"
} %>
<span class="input-group-text bg-white">%</span>
</div>
<small class="text-muted">What percentage to deposit (1-100)</small>
</div>
<div class="d-grid">
<%= submit_tag "Add Account", {
id: "dd_form_btn",
class: "btn btn-success btn-lg"
} %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-success);">
<small class="text-muted">
<i class="bi bi-shield-lock-fill text-success me-1"></i>
<strong>Secure:</strong> All account information is encrypted
</small>
</div>
<% end %>
</div>
</div>
<!-- Decrypt Form -->
<div class="card shadow-sm" style="border-left: 4px solid var(--rg-warning);">
<div class="card-header py-3" style="background: linear-gradient(135deg, rgba(255, 183, 3, 0.05), rgba(251, 133, 0, 0.05));">
<h4 class="mb-0">
<i class="bi bi-unlock text-warning"></i> Decrypt Account Number
</h4>
<p class="text-muted mb-0 small mt-1">View unencrypted account number</p>
</div>
<div class="card-body p-4">
<%= form_tag "#", { class: "needs-validation", id: "decrypt_form" } do %>
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-key-fill text-warning me-2"></i>Encrypted Account Number
</label>
<div class="input-group input-group-lg">
<%= text_field_tag :value_to_decrypt, params[:value_to_decrypt], {
placeholder: "Paste encrypted value",
class: "form-control"
} %>
<span class="input-group-text bg-white">
<i class="bi bi-lock"></i>
</span>
</div>
<small class="text-muted">Copy from the table on the right</small>
</div>
<div class="d-grid">
<%= submit_tag "Decrypt", {
id: "decrypt_btn",
class: "btn btn-warning btn-lg"
} %>
</div>
<div class="mt-3 p-3 rounded" style="background: var(--rg-light); border-left: 3px solid var(--rg-warning);">
<small class="text-muted">
<i class="bi bi-info-circle-fill text-warning me-1"></i>
Decryption is for your convenience and security verification
</small>
</div>
<% end %>
</div>
</div>
</div>
<!-- Right Column - Accounts Table -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<h4 class="mb-0">
<i class="bi bi-list-ul text-primary"></i> Direct Deposit Accounts
</h4>
<p class="text-muted mb-0 small mt-1">Your configured bank accounts</p>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="encrypted_acct_question">
<i class="bi bi-question-circle"></i> Why Encrypted?
</button>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="data_table">
<thead class="table-light">
<tr>
<th style="width: 35%;">
<i class="bi bi-shield-lock me-2"></i>Encrypted Account Number
</th>
<th style="width: 25%;">
<i class="bi bi-diagram-3 me-2"></i>Routing Number
</th>
<th style="width: 20%;">
<i class="bi bi-percent me-2"></i>Deposit %
</th>
<th style="width: 20%;">
<i class="bi bi-gear me-2"></i>Actions
</th>
</tr>
</thead>
<tbody>
<!-- DataTable will populate this -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Info Cards -->
<div class="row mt-3 g-3">
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #579da9;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-lightning-charge-fill text-primary"></i> Instant Access
</h6>
<p class="card-text small text-muted">
Your paycheck is deposited directly into your account on payday.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid #1e825e;">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-shield-check text-success"></i> Secure & Encrypted
</h6>
<p class="card-text small text-muted">
All banking information is encrypted using industry-standard security.
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm h-100" style="border-left: 3px solid var(--rg-warning);">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-piggy-bank text-warning"></i> Split Deposits
</h6>
<p class="card-text small text-muted">
Allocate different percentages of your pay to multiple accounts.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%= javascript_include_tag "jquery.dataTables.min.js" %>
<script type="text/javascript">
/*
buildDeleteLink accepts a direct deposit ID and builds a link that enables
a user to delete that direct deposit entry
*/
function buildDeleteLink(dd_id){
var link = '<a href="/users/' + '<%= current_user.id %>' + '/pay/'+ dd_id + '" data-method="delete" rel="nofollow" class="btn btn-sm btn-outline-danger delete-row">' +
'<i class="bi bi-trash"></i> Delete</a>'
return link
};
/*
parseDirectDepositInfo accepts the response object and parses the JSON response, then
populates the direct deposit data table.
*/
function parseDirectDepostInfo(response){
var msg = jQuery.parseJSON(JSON.stringify(response));
var table = $('#data_table').DataTable();
$.each(msg.user, function(index, val){
table.row.add( [
'<code class="text-monospace">' + val.bank_account_num + '</code>',
'<span class="badge bg-light text-dark">' + val.bank_routing_num + '</span>',
'<span class="badge bg-success">' + val.percent_of_deposit + '%</span>',
buildDeleteLink(val.id)
] );
});
table.draw();
};
/*
createDataTable initializes the dd table as a datatable
*/
function createDataTable(){
// Check if DataTable is already initialized
if ($.fn.DataTable.isDataTable('#data_table')) {
$('#data_table').DataTable().destroy();
}
$('#data_table').DataTable({
"sPaginationType": "full_numbers",
"language": {
"emptyTable": "No direct deposit accounts configured yet"
},
"autoWidth": false,
"searching": true,
"ordering": true
});
};
/*
populateTable will first clear the existing dd table, then call the appropriate
endpoint to retrieve direct deposit entries and finally, provide parseDirectDepositInfo
with the response from the endpoint in order to populate the data table.
*/
function populateTable() {
var table = $('#data_table').DataTable();
table.clear();
$.ajax({
url: <%= sanitize(user_pay_path(:format => "json", user_id: current_user.id, id: current_user.id).inspect) %>,
type: "GET",
success: function(response) {
parseDirectDepostInfo(response);
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
};
/*
This function doesn't really work right now but is supposed to offer the user a
"delete confirmation" message
*/
$(document).on('click', '.delete-row', function (e) {
var conf = confirm('Are you sure you want to delete this account?');
if (!conf) {
e.preventDefault();
return false;
}
});
/*
decryptShow parses the json response from the application and then renders
a decrypted version of the user's account number
*/
function decryptShow(response){
var msg = jQuery.parseJSON(JSON.stringify(response));
// Modern alert using Bootstrap modal-like appearance
var alertHtml = '<div class="alert alert-info alert-dismissible fade show" role="alert" style="position: fixed; top: 100px; left: 50%; transform: translateX(-50%); z-index: 9999; min-width: 400px; box-shadow: 0 4px 16px rgba(0,0,0,0.2);">' +
'<div class="d-flex align-items-center">' +
'<i class="bi bi-unlock-fill me-3" style="font-size: 2rem;"></i>' +
'<div>' +
'<h5 class="alert-heading mb-1">Decrypted Account Number</h5>' +
'<p class="mb-0"><strong style="font-size: 1.2rem; font-family: monospace;">' + msg.account_num + '</strong></p>' +
'</div>' +
'</div>' +
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' +
'</div>';
$('body').append(alertHtml);
// Auto-remove after 5 seconds
setTimeout(function() {
$('.alert').fadeOut(function() {
$(this).remove();
});
}, 5000);
};
/*
This function overrides the decrypt buttons (submit button's) native behavior,
allowing an ajax call to be made with the decrypt_form's inputs which is decrypted
server side with a JSON response containing the decrypted value. The decrypted value is
then passed to decryptShow();
*/
$("#decrypt_btn").click(function(event){
var valuesToSubmit = $("#decrypt_form").serialize();
event.preventDefault();
$.ajax({
url: <%= sanitize(decrypted_bank_acct_num_user_pay_index_path(:format => "json", user_id: current_user.id).inspect) %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
decryptShow(response);
$('#decrypt_form')[0].reset();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
/*
This function overrides the dd_form_btn's native behavior in order to submit an ajax request
that updates the user's direct deposit information. Upon success, the populateTable() function
is called in order to update the dataTable on the page to reflect the latest entry.
*/
$("#dd_form_btn").click(function(event) {
var valuesToSubmit = $("#bank_info_form").serialize();
event.preventDefault();
$.ajax({
url: <%= sanitize(update_dd_info_user_pay_index_path(:format => "json").inspect) %>,
data: valuesToSubmit,
type: "POST",
success: function(response) {
$('#success').show(500).delay(1500).fadeOut();
$('#bank_info_form')[0].reset();
populateTable();
},
error: function(event) {
$('#failure').show(500).delay(1500).fadeOut();
}
});
});
$("#encrypted_acct_question").click(function(event) {
event.preventDefault();
// Create modern Bootstrap modal-like alert
var modalHtml = '<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">' +
'<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content">' +
'<div class="modal-header" style="background: linear-gradient(135deg, rgba(69, 123, 157, 0.1), rgba(29, 53, 87, 0.1)); border-left: 4px solid var(--rg-secondary);">' +
'<h5 class="modal-title"><i class="bi bi-shield-lock-fill text-primary me-2"></i>Why Are Account Numbers Encrypted?</h5>' +
'<button type="button" class="btn-close" onclick="$(this).closest(\'.modal\').remove();"></button>' +
'</div>' +
'<div class="modal-body">' +
'<p class="mb-3"><i class="bi bi-check-circle-fill text-success me-2"></i><strong>For your safety</strong>, your account number is stored encrypted in our database and presented to you in an encrypted form.</p>' +
'<p class="mb-0"><i class="bi bi-unlock-fill text-warning me-2"></i><strong>For your convenience</strong>, you can decrypt your bank account number at any time using our conveniently located decryption function.</p>' +
'</div>' +
'<div class="modal-footer">' +
'<button type="button" class="btn btn-primary" onclick="$(this).closest(\'.modal\').remove();">Got It</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
$('body').append(modalHtml);
});
/*
Make the sidebar element "Pay" active.
*/
function makeActive(){
$('li[id="pay"]').addClass('active');
};
/*
Initialize page - called on both ready and turbolinks:load
*/
function initializePage() {
makeActive();
createDataTable();
populateTable();
}
// Handle normal page loads
$(document).ready(function() {
initializePage();
});
// Handle Turbolinks page loads
$(document).on('turbolinks:load', function() {
initializePage();
});
</script>
<style>
/* DataTables styling adjustments */
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.375rem 0.75rem;
margin: 0 0.125rem;
border-radius: 0.5rem;
border: 1px solid #dee2e6;
background: white;
color: var(--rg-dark);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: var(--rg-primary);
color: white;
border-color: var(--rg-primary);
}
.dataTables_filter input {
border-radius: 0.75rem;
border: 2px solid #e9ecef;
padding: 0.5rem 1rem;
}
.dataTables_filter input:focus {
border-color: var(--rg-primary);
outline: none;
box-shadow: 0 0 0 3px rgba(230, 57, 70, 0.1);
}
.text-monospace {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
/* Table hover effect */
#data_table tbody tr {
transition: background-color 0.2s ease;
}
#data_table tbody tr:hover {
background-color: rgba(230, 57, 70, 0.03);
}
</style>