Compare commits

...

5 Commits

Author SHA1 Message Date
jChenvan b32359dbdf Previous/next button for pending entries only
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-18 19:44:18 -04:00
jChenvan 350e306985 Add prev_pending_id and next_pending_id 2025-07-18 19:36:33 -04:00
jChenvan 9bb74a84df Add batch approval/rejection 2025-07-18 19:26:45 -04:00
jChenvan bfd8796f2b Add select feature 2025-07-17 20:59:03 -04:00
jChenvan 77765d8605 Fix incorrect db info 2025-07-17 20:58:49 -04:00
5 changed files with 126 additions and 8 deletions

View File

@ -308,6 +308,8 @@ def view_transaction(id):
transaction = None transaction = None
prev_id = None prev_id = None
next_id = None next_id = None
prev_pending_id = None
next_pending_id = None
try: try:
with conn.cursor() as cur: with conn.cursor() as cur:
@ -325,6 +327,14 @@ def view_transaction(id):
cur.execute('SELECT id FROM transactions WHERE id > %s ORDER BY id ASC LIMIT 1', (id,)) cur.execute('SELECT id FROM transactions WHERE id > %s ORDER BY id ASC LIMIT 1', (id,))
next_result = cur.fetchone() next_result = cur.fetchone()
next_id = next_result['id'] if next_result else None next_id = next_result['id'] if next_result else None
cur.execute('SELECT id FROM transactions WHERE id < %s AND approved = FALSE ORDER BY id DESC LIMIT 1', (id,))
prev_pending_result = cur.fetchone()
prev_pending_id = prev_pending_result['id'] if prev_pending_result else None
cur.execute('SELECT id FROM transactions WHERE id > %s AND approved = FALSE ORDER BY id ASC LIMIT 1', (id,))
next_pending_result = cur.fetchone()
next_pending_id = next_pending_result['id'] if next_pending_result else None
except Exception as e: except Exception as e:
logger.error(f"Database error: {e}") logger.error(f"Database error: {e}")
abort(404) abort(404)
@ -337,7 +347,7 @@ def view_transaction(id):
if transaction is None: if transaction is None:
abort(404) abort(404)
return render_template('view_transaction.html', transaction=transaction, documents=documents, prev_id=prev_id, next_id=next_id, version=VERSION, source = source) return render_template('view_transaction.html', transaction=transaction, documents=documents, prev_id=prev_id, next_id=next_id, prev_pending_id=prev_pending_id, next_pending_id=next_pending_id, version=VERSION, source = source)
@app.route('/document/<int:document_id>') @app.route('/document/<int:document_id>')
def view_document(document_id): def view_document(document_id):

View File

@ -4,11 +4,11 @@ import os
def init_db(): def init_db():
# Database connection parameters # Database connection parameters
conn = psycopg2.connect( conn = psycopg2.connect(
host="192.168.1.119", host="db",
port=5433, port=5432,
dbname="testdb", dbname="ploughshares",
user="testuser", user="ploughshares",
password="testpass" password="ploughshares_password"
) )
conn.autocommit = True conn.autocommit = True
cursor = conn.cursor() cursor = conn.cursor()

View File

@ -1,6 +1,6 @@
-- Drop tables if they exist -- Drop tables if they exist
DROP TABLE IF EXISTS transaction_documents; DROP TABLE IF EXISTS transaction_documents CASCADE;
DROP TABLE IF EXISTS transactions; DROP TABLE IF EXISTS transactions CASCADE;
-- Create transactions table -- Create transactions table
CREATE TABLE IF NOT EXISTS transactions ( CREATE TABLE IF NOT EXISTS transactions (

View File

@ -2,6 +2,23 @@
{% block title %}Transactions Pending Approval - Project Ploughshares{% endblock %} {% block title %}Transactions Pending Approval - Project Ploughshares{% endblock %}
{% block styles %}
<style>
.items-selected {
display: flex;
background-color: var(--ploughshares-blue);
padding: 10px;
border-radius: 5px;
color: white;
}
.items-selected p {
flex: 1;
margin: 0;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<div class="card"> <div class="card">
@ -17,6 +34,15 @@
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="items-selected">
<p><span>0</span> items selected</p>
<button class="approve">approve</button>
<button class="reject">reject</button>
<dialog class="reject">
<p>reject records? this will delete them from the database permanently.</p>
<button>delete permanently</button>
</dialog>
</div>
{% if transactions %} {% if transactions %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
@ -31,6 +57,7 @@
<th>Created At</th> <th>Created At</th>
<th>Documents</th> <th>Documents</th>
<th>Actions</th> <th>Actions</th>
<th>Select</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -120,6 +147,7 @@
</div> </div>
</div> </div>
</td> </td>
<td><input type="checkbox" class="transaction-checkbox" data-transaction-id="{{ transaction['id'] }}"></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -137,6 +165,55 @@
{% block scripts %} {% block scripts %}
<script nonce="{{ csp_nonce() }}"> <script nonce="{{ csp_nonce() }}">
const checkedTransactions = [];
const counter = document.querySelector(".items-selected span");
function handleCheck(transactionId, checked) {
const index = checkedTransactions.findIndex(x=>x===transactionId);
if (index >= 0) checkedTransactions.splice(index, 1);
if (checked) checkedTransactions.push(transactionId);
counter.textContent = checkedTransactions.length;
}
const approveButton = document.querySelector(".items-selected button.approve");
const rejectButton = document.querySelector(".items-selected button.reject");
const rejectDialog = document.querySelector(".items-selected dialog.reject");
const rejectDialogButton = document.querySelector(".items-selected dialog.reject button");
approveButton.addEventListener("click", async () => {
const promises = checkedTransactions.map(async id=>{
await fetch(
`/api/transaction/${id}/approve`,
{
method: "POST",
}
)
});
await Promise.all(promises);
location.reload();
});
rejectButton.addEventListener("click", () => rejectDialog.showModal());
rejectDialog.addEventListener("click", e => {
if (e.target === rejectDialog) rejectDialog.close();
});
rejectDialogButton.addEventListener("click", async () => {
const promises = checkedTransactions.map(async id=>{
await fetch(
`/api/transaction/${id}`,
{
method: "DELETE",
}
)
});
await Promise.all(promises);
location.reload();
});
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Refresh button functionality // Refresh button functionality
const refreshBtn = document.getElementById('refreshBtn'); const refreshBtn = document.getElementById('refreshBtn');
@ -188,6 +265,15 @@
} }
}); });
}); });
// Attach event listeners to checkboxes
document.querySelectorAll('.transaction-checkbox').forEach(function(checkbox) {
const transactionId = checkbox.getAttribute("data-transaction-id");
handleCheck(transactionId, checkbox.checked);
checkbox.addEventListener('change', e => {
handleCheck(transactionId, e.target.checked);
});
});
// Helper function to check if user is typing in an input field // Helper function to check if user is typing in an input field
function isUserTyping() { function isUserTyping() {

View File

@ -120,6 +120,28 @@
</button> </button>
{% endif %} {% endif %}
</div> </div>
<div class="d-flex justify-content-between mb-3">
{% if prev_pending_id %}
<a href="{{ url_for('view_transaction', id=prev_pending_id) }}" class="btn btn-outline-primary" aria-label="Previous Pending transaction" title="Previous Pending Transaction">
<i class="bi bi-arrow-left"></i> Previous Pending
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No previous pending transaction" title="Previous Pending Transaction">
<i class="bi bi-arrow-left"></i> Previous Pending
</button>
{% endif %}
{% if next_pending_id %}
<a href="{{ url_for('view_transaction', id=next_pending_id) }}" class="btn btn-outline-primary" aria-label="Next Pending transaction" title="Next Pending Transaction">
Next Pending <i class="bi bi-arrow-right"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled aria-label="No next Pending transaction" title="Next Pending Transaction">
Next Pending <i class="bi bi-arrow-right"></i>
</button>
{% endif %}
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">