Add keyboard shortcuts for improved user experience
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
679c51263d
commit
5dc4eea2a6
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Keyboard shortcuts styling for Project Ploughshares Transaction Management System
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Keyboard key styling */
|
||||||
|
kbd {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
line-height: 1;
|
||||||
|
color: #212529;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
box-shadow: inset 0 -0.1rem 0 rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hotkey reference button */
|
||||||
|
.hotkey-help-btn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1030;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotkey-help-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip for hotkeys */
|
||||||
|
[data-hotkey]::after {
|
||||||
|
content: attr(data-hotkey);
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: #6c757d;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hotkey badge in buttons */
|
||||||
|
.btn .hotkey-badge {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
vertical-align: middle;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hotkey modal styling */
|
||||||
|
.hotkey-table td:first-child {
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotkey-table td {
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hotkey-help-btn {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-hotkey]::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,411 @@
|
||||||
|
/**
|
||||||
|
* Keyboard shortcuts for Project Ploughshares Transaction Management System
|
||||||
|
*/
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize hotkeys
|
||||||
|
initHotkeys();
|
||||||
|
|
||||||
|
// Add hotkey tooltips to elements
|
||||||
|
addHotkeyTooltips();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize keyboard shortcuts
|
||||||
|
*/
|
||||||
|
function initHotkeys() {
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
// Skip if user is typing in an input field, textarea or has a modal open
|
||||||
|
if (isUserTyping() || isModalOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current page based on URL
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
// Global shortcuts (available on all pages)
|
||||||
|
switch (event.key) {
|
||||||
|
case 'h': // Help - Show hotkey reference
|
||||||
|
event.preventDefault();
|
||||||
|
showHotkeyReference();
|
||||||
|
break;
|
||||||
|
case 'g': // Go to
|
||||||
|
if (event.altKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
showGoToModal();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page-specific shortcuts
|
||||||
|
if (currentPath === '/' || currentPath === '/index.html') {
|
||||||
|
// Transaction list page
|
||||||
|
handleTransactionListHotkeys(event);
|
||||||
|
} else if (currentPath.startsWith('/transaction/') && !currentPath.includes('/edit')) {
|
||||||
|
// Transaction view page
|
||||||
|
handleTransactionViewHotkeys(event);
|
||||||
|
} else if (currentPath === '/pending-approval') {
|
||||||
|
// Pending approval page
|
||||||
|
handlePendingApprovalHotkeys(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle hotkeys for the transaction list page
|
||||||
|
*/
|
||||||
|
function handleTransactionListHotkeys(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'n': // New transaction
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = '/transaction/add';
|
||||||
|
break;
|
||||||
|
case 'p': // Pending approval
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = '/pending-approval';
|
||||||
|
break;
|
||||||
|
case 'f': // Focus search
|
||||||
|
event.preventDefault();
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'r': // Refresh
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle hotkeys for the transaction view page
|
||||||
|
*/
|
||||||
|
function handleTransactionViewHotkeys(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'e': // Edit
|
||||||
|
event.preventDefault();
|
||||||
|
const editBtn = document.querySelector('a[href^="/transaction/"][href$="/edit"]');
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'b': // Back to list
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = '/';
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft': // Previous transaction
|
||||||
|
event.preventDefault();
|
||||||
|
const prevBtn = document.querySelector('a[aria-label="Previous transaction"]');
|
||||||
|
if (prevBtn && !prevBtn.classList.contains('disabled') && !prevBtn.hasAttribute('disabled')) {
|
||||||
|
prevBtn.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowRight': // Next transaction
|
||||||
|
event.preventDefault();
|
||||||
|
const nextBtn = document.querySelector('a[aria-label="Next transaction"]');
|
||||||
|
if (nextBtn && !nextBtn.classList.contains('disabled') && !nextBtn.hasAttribute('disabled')) {
|
||||||
|
nextBtn.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'a': // Approve
|
||||||
|
event.preventDefault();
|
||||||
|
const approveBtn = document.querySelector('button[data-bs-target="#approveModal"]');
|
||||||
|
if (approveBtn) {
|
||||||
|
approveBtn.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle hotkeys for the pending approval page
|
||||||
|
*/
|
||||||
|
function handlePendingApprovalHotkeys(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'b': // Back to list
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = '/';
|
||||||
|
break;
|
||||||
|
case 'r': // Refresh
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
// Number keys 1-9 to quickly view transactions
|
||||||
|
case '1':
|
||||||
|
case '2':
|
||||||
|
case '3':
|
||||||
|
case '4':
|
||||||
|
case '5':
|
||||||
|
case '6':
|
||||||
|
case '7':
|
||||||
|
case '8':
|
||||||
|
case '9':
|
||||||
|
event.preventDefault();
|
||||||
|
const index = parseInt(event.key) - 1;
|
||||||
|
const viewButtons = document.querySelectorAll('a[aria-label^="View transaction"]');
|
||||||
|
if (viewButtons.length > index) {
|
||||||
|
viewButtons[index].click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is currently typing in an input field
|
||||||
|
*/
|
||||||
|
function isUserTyping() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
return activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a modal is currently open
|
||||||
|
*/
|
||||||
|
function isModalOpen() {
|
||||||
|
return document.querySelector('.modal.show') !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a modal with hotkey reference
|
||||||
|
*/
|
||||||
|
function showHotkeyReference() {
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
let modal = document.getElementById('hotkeyReferenceModal');
|
||||||
|
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.id = 'hotkeyReferenceModal';
|
||||||
|
modal.className = 'modal fade';
|
||||||
|
modal.tabIndex = '-1';
|
||||||
|
modal.setAttribute('aria-labelledby', 'hotkeyReferenceModalLabel');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="hotkeyReferenceModalLabel">Keyboard Shortcuts</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Global Shortcuts</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td><kbd>H</kbd></td>
|
||||||
|
<td>Show this help</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>Alt</kbd> + <kbd>G</kbd></td>
|
||||||
|
<td>Go to page</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h6>Transaction List</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td><kbd>N</kbd></td>
|
||||||
|
<td>New transaction</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>P</kbd></td>
|
||||||
|
<td>Pending approval</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>F</kbd></td>
|
||||||
|
<td>Focus search</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>R</kbd></td>
|
||||||
|
<td>Refresh page</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Transaction View</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td><kbd>E</kbd></td>
|
||||||
|
<td>Edit transaction</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>B</kbd></td>
|
||||||
|
<td>Back to list</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>←</kbd></td>
|
||||||
|
<td>Previous transaction</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>→</kbd></td>
|
||||||
|
<td>Next transaction</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>A</kbd></td>
|
||||||
|
<td>Approve transaction</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h6>Pending Approval</h6>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<tr>
|
||||||
|
<td><kbd>B</kbd></td>
|
||||||
|
<td>Back to list</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>R</kbd></td>
|
||||||
|
<td>Refresh page</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>1</kbd>-<kbd>9</kbd></td>
|
||||||
|
<td>View transaction #1-9</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
|
bsModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a modal for quick navigation
|
||||||
|
*/
|
||||||
|
function showGoToModal() {
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
let modal = document.getElementById('goToModal');
|
||||||
|
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.id = 'goToModal';
|
||||||
|
modal.className = 'modal fade';
|
||||||
|
modal.tabIndex = '-1';
|
||||||
|
modal.setAttribute('aria-labelledby', 'goToModalLabel');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="goToModalLabel">Go To</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="/" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
Home
|
||||||
|
<span class="badge bg-primary">H</span>
|
||||||
|
</a>
|
||||||
|
<a href="/transaction/add" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
New Transaction
|
||||||
|
<span class="badge bg-primary">N</span>
|
||||||
|
</a>
|
||||||
|
<a href="/pending-approval" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
Pending Approval
|
||||||
|
<span class="badge bg-primary">P</span>
|
||||||
|
</a>
|
||||||
|
<a href="/api-docs" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
API Documentation
|
||||||
|
<span class="badge bg-primary">D</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
|
bsModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add hotkey tooltips to elements
|
||||||
|
*/
|
||||||
|
function addHotkeyTooltips() {
|
||||||
|
// Get the current page based on URL
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
// Add tooltips for global elements
|
||||||
|
const helpLink = document.createElement('div');
|
||||||
|
helpLink.className = 'position-fixed bottom-0 end-0 p-3';
|
||||||
|
helpLink.innerHTML = `
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" onclick="showHotkeyReference()"
|
||||||
|
title="Show Keyboard Shortcuts (H)">
|
||||||
|
<i class="bi bi-keyboard"></i> <span class="d-none d-md-inline">Keyboard Shortcuts</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(helpLink);
|
||||||
|
|
||||||
|
// Page-specific tooltips
|
||||||
|
if (currentPath === '/' || currentPath === '/index.html') {
|
||||||
|
// Transaction list page
|
||||||
|
const newTransactionBtn = document.querySelector('a[href="/transaction/add"]');
|
||||||
|
if (newTransactionBtn) {
|
||||||
|
newTransactionBtn.setAttribute('title', 'New Transaction (N)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingApprovalLink = document.querySelector('a[href="/pending-approval"]');
|
||||||
|
if (pendingApprovalLink) {
|
||||||
|
pendingApprovalLink.setAttribute('title', 'Pending Approval (P)');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (currentPath.startsWith('/transaction/') && !currentPath.includes('/edit')) {
|
||||||
|
// Transaction view page
|
||||||
|
const editBtn = document.querySelector('a[href^="/transaction/"][href$="/edit"]');
|
||||||
|
if (editBtn) {
|
||||||
|
editBtn.setAttribute('title', 'Edit Transaction (E)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const backBtn = document.querySelector('a[href="/"]');
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.setAttribute('title', 'Back to List (B)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevBtn = document.querySelector('a[aria-label="Previous transaction"]');
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.setAttribute('title', 'Previous Transaction (←)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextBtn = document.querySelector('a[aria-label="Next transaction"]');
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.setAttribute('title', 'Next Transaction (→)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveBtn = document.querySelector('button[data-bs-target="#approveModal"]');
|
||||||
|
if (approveBtn) {
|
||||||
|
approveBtn.setAttribute('title', 'Approve Transaction (A)');
|
||||||
|
}
|
||||||
|
} else if (currentPath === '/pending-approval') {
|
||||||
|
// Pending approval page
|
||||||
|
const backBtn = document.querySelector('a[href="/"]');
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.setAttribute('title', 'Back to List (B)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add numbers to view buttons
|
||||||
|
const viewButtons = document.querySelectorAll('a[aria-label^="View transaction"]');
|
||||||
|
viewButtons.forEach((btn, index) => {
|
||||||
|
if (index < 9) {
|
||||||
|
btn.setAttribute('title', `View Transaction (${index + 1})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/hotkeys.css') }}">
|
||||||
<style>
|
<style>
|
||||||
.navbar-brand img {
|
.navbar-brand img {
|
||||||
max-height: 40px;
|
max-height: 40px;
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/hotkeys.js') }}"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
<script nonce="{{ csp_nonce() }}">
|
<script nonce="{{ csp_nonce() }}">
|
||||||
// Add ARIA attributes to dynamically created elements
|
// Add ARIA attributes to dynamically created elements
|
||||||
|
|
|
@ -7,9 +7,20 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h2>Transactions</h2>
|
<h2>Transactions</h2>
|
||||||
<a href="{{ url_for('create_transaction') }}" class="btn btn-success">
|
<div class="d-flex gap-2">
|
||||||
<i class="bi bi-plus-lg"></i> New Transaction
|
<div class="input-group">
|
||||||
|
<input type="text" id="searchInput" class="form-control" placeholder="Search transactions..." aria-label="Search transactions">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="searchButton" title="Search (F)">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('pending_approval') }}" class="btn btn-warning" title="Pending Approval (P)">
|
||||||
|
<i class="bi bi-clock-history"></i> <span class="d-none d-md-inline">Pending</span> <span class="badge bg-light text-dark" id="pending-count-btn"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('create_transaction') }}" class="btn btn-success" title="New Transaction (N)">
|
||||||
|
<i class="bi bi-plus-lg"></i> <span class="d-none d-md-inline">New Transaction</span> <span class="d-md-none">New</span> <span class="d-none d-md-inline">(N)</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
|
@ -66,6 +77,7 @@
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
const searchButton = document.getElementById('searchButton');
|
const searchButton = document.getElementById('searchButton');
|
||||||
const tableRows = document.querySelectorAll('tbody tr');
|
const tableRows = document.querySelectorAll('tbody tr');
|
||||||
|
const pendingCountBtn = document.getElementById('pending-count-btn');
|
||||||
|
|
||||||
function filterTable() {
|
function filterTable() {
|
||||||
const searchTerm = searchInput.value.toLowerCase();
|
const searchTerm = searchInput.value.toLowerCase();
|
||||||
|
@ -90,6 +102,41 @@
|
||||||
filterTable();
|
filterTable();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add keyboard shortcut focus
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'f' && !isUserTyping()) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update pending count badge on button
|
||||||
|
if (pendingCountBtn) {
|
||||||
|
fetch('/api/transactions/pending')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const count = data.length;
|
||||||
|
if (count > 0) {
|
||||||
|
pendingCountBtn.textContent = count;
|
||||||
|
pendingCountBtn.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
pendingCountBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching pending transactions:', error);
|
||||||
|
pendingCountBtn.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if user is typing in an input field
|
||||||
|
function isUserTyping() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
return activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.isContentEditable;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h2>Transactions Pending Approval</h2>
|
<h2>Transactions Pending Approval</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
<a href="{{ url_for('index') }}" class="btn btn-secondary" title="Back to List (B)">
|
||||||
<i class="bi bi-arrow-left"></i> Back to All Transactions
|
<i class="bi bi-arrow-left"></i> Back to All Transactions <span class="d-none d-md-inline">(B)</span>
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="refreshBtn" title="Refresh (R)">
|
||||||
|
<i class="bi bi-arrow-clockwise"></i> Refresh <span class="d-none d-md-inline">(R)</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
@ -41,8 +44,8 @@
|
||||||
<td>{{ transaction['created_at'].strftime('%Y-%m-%d %H:%M') if transaction['created_at'] else 'N/A' }}</td>
|
<td>{{ transaction['created_at'].strftime('%Y-%m-%d %H:%M') if transaction['created_at'] else 'N/A' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a href="{{ url_for('view_transaction', id=transaction['id']) }}" class="btn btn-sm btn-info" aria-label="View transaction {{ transaction['id'] }}">
|
<a href="{{ url_for('view_transaction', id=transaction['id']) }}" class="btn btn-sm btn-info" aria-label="View transaction {{ transaction['id'] }}" title="View ({{ loop.index if loop.index < 10 else '' }})">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>{% if loop.index < 10 %} <span class="badge bg-light text-dark">{{ loop.index }}</span>{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-sm btn-warning" aria-label="Edit transaction {{ transaction['id'] }}">
|
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-sm btn-warning" aria-label="Edit transaction {{ transaction['id'] }}">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
|
@ -95,7 +98,41 @@
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script nonce="{{ csp_nonce() }}">
|
<script nonce="{{ csp_nonce() }}">
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Add any specific scripts for the pending approval page
|
// Refresh button functionality
|
||||||
|
const refreshBtn = document.getElementById('refreshBtn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', function() {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add keyboard shortcut for refresh
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'r' && !isUserTyping()) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add keyboard shortcuts for quick navigation to transactions (1-9)
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (!isUserTyping() && /^[1-9]$/.test(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
const index = parseInt(e.key) - 1;
|
||||||
|
const viewButtons = document.querySelectorAll('a[aria-label^="View transaction"]');
|
||||||
|
if (viewButtons.length > index) {
|
||||||
|
viewButtons[index].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to check if user is typing in an input field
|
||||||
|
function isUserTyping() {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
return activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.isContentEditable;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -8,11 +8,11 @@
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h2>Transaction #{{ transaction['id'] }} - Project Ploughshares</h2>
|
<h2>Transaction #{{ transaction['id'] }} - Project Ploughshares</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-warning">
|
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-warning" title="Edit Transaction (E)">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit <span class="d-none d-md-inline">(E)</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
<a href="{{ url_for('index') }}" class="btn btn-secondary" title="Back to List (B)">
|
||||||
<i class="bi bi-arrow-left"></i> Back to List
|
<i class="bi bi-arrow-left"></i> Back to List <span class="d-none d-md-inline">(B)</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||||
Pending Approval
|
Pending Approval
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal">
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal" title="Approve Transaction (A)">
|
||||||
<i class="bi bi-check-lg"></i> Approve
|
<i class="bi bi-check-lg"></i> Approve <span class="d-none d-md-inline">(A)</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Approve Modal -->
|
<!-- Approve Modal -->
|
||||||
|
@ -63,22 +63,22 @@
|
||||||
<!-- Navigation buttons -->
|
<!-- Navigation buttons -->
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
{% if prev_id %}
|
{% if prev_id %}
|
||||||
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction">
|
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction" title="Previous Transaction (←)">
|
||||||
<i class="bi bi-arrow-left"></i> Previous
|
<i class="bi bi-arrow-left"></i> Previous <span class="d-none d-md-inline">(←)</span>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction">
|
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction" title="Previous Transaction (←)">
|
||||||
<i class="bi bi-arrow-left"></i> Previous
|
<i class="bi bi-arrow-left"></i> Previous <span class="d-none d-md-inline">(←)</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if next_id %}
|
{% if next_id %}
|
||||||
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction">
|
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction" title="Next Transaction (→)">
|
||||||
Next <i class="bi bi-arrow-right"></i>
|
Next <span class="d-none d-md-inline">(→)</span> <i class="bi bi-arrow-right"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction">
|
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction" title="Next Transaction (→)">
|
||||||
Next <i class="bi bi-arrow-right"></i>
|
Next <span class="d-none d-md-inline">(→)</span> <i class="bi bi-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -159,26 +159,26 @@
|
||||||
<!-- Bottom navigation buttons for easier access -->
|
<!-- Bottom navigation buttons for easier access -->
|
||||||
<div class="d-flex justify-content-between mt-3">
|
<div class="d-flex justify-content-between mt-3">
|
||||||
{% if prev_id %}
|
{% if prev_id %}
|
||||||
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction">
|
<a href="{{ url_for('view_transaction', id=prev_id) }}" class="btn btn-outline-primary" aria-label="Previous transaction" title="Previous Transaction (←)">
|
||||||
<i class="bi bi-arrow-left"></i> Previous
|
<i class="bi bi-arrow-left"></i> Previous <span class="d-none d-md-inline">(←)</span>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction">
|
<button class="btn btn-outline-secondary" disabled aria-label="No previous transaction" title="Previous Transaction (←)">
|
||||||
<i class="bi bi-arrow-left"></i> Previous
|
<i class="bi bi-arrow-left"></i> Previous <span class="d-none d-md-inline">(←)</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
<a href="{{ url_for('index') }}" class="btn btn-secondary" title="Back to List (B)">
|
||||||
<i class="bi bi-list"></i> All Transactions
|
<i class="bi bi-list"></i> All Transactions <span class="d-none d-md-inline">(B)</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if next_id %}
|
{% if next_id %}
|
||||||
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction">
|
<a href="{{ url_for('view_transaction', id=next_id) }}" class="btn btn-outline-primary" aria-label="Next transaction" title="Next Transaction (→)">
|
||||||
Next <i class="bi bi-arrow-right"></i>
|
Next <span class="d-none d-md-inline">(→)</span> <i class="bi bi-arrow-right"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction">
|
<button class="btn btn-outline-secondary" disabled aria-label="No next transaction" title="Next Transaction (→)">
|
||||||
Next <i class="bi bi-arrow-right"></i>
|
Next <span class="d-none d-md-inline">(→)</span> <i class="bi bi-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,3 +3,4 @@ Thu Jul 3 13:40:53 EDT 2025: Version changed from 0.1.1 to 0.1.2
|
||||||
Fri Jul 4 16:04:29 EDT 2025: Version changed from 0.1.2 to 0.2.0
|
Fri Jul 4 16:04:29 EDT 2025: Version changed from 0.1.2 to 0.2.0
|
||||||
Fri Jul 4 20:03:15 EDT 2025: Version changed from 0.2.0 to 0.2.1
|
Fri Jul 4 20:03:15 EDT 2025: Version changed from 0.2.0 to 0.2.1
|
||||||
Fri Jul 4 22:25:55 EDT 2025: Version changed from 0.2.1 to 0.3.0
|
Fri Jul 4 22:25:55 EDT 2025: Version changed from 0.2.1 to 0.3.0
|
||||||
|
Fri Jul 4 22:33:48 EDT 2025: Version changed from 0.3.0 to 0.3.1
|
||||||
|
|
Loading…
Reference in New Issue