Add transaction approval system
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
a64ac45046
commit
679c51263d
|
@ -302,6 +302,136 @@ def view_transaction(id):
|
|||
|
||||
return render_template('view_transaction.html', transaction=transaction, prev_id=prev_id, next_id=next_id, version=VERSION)
|
||||
|
||||
@app.route('/pending-approval')
|
||||
def pending_approval():
|
||||
"""
|
||||
Display transactions pending approval
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
if conn is None:
|
||||
flash("Database connection error", "error")
|
||||
return render_template('pending_approval.html', transactions=[], version=VERSION)
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT * FROM transactions WHERE approved = FALSE ORDER BY id DESC')
|
||||
transactions = cur.fetchall()
|
||||
except Exception as e:
|
||||
logger.error(f"Database error: {e}")
|
||||
flash(f"Database error: {e}", "error")
|
||||
transactions = []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return render_template('pending_approval.html', transactions=transactions, version=VERSION)
|
||||
|
||||
@app.route('/transaction/<int:id>/approve', methods=['POST'])
|
||||
def approve_transaction(id):
|
||||
"""
|
||||
Approve a transaction
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
if conn is None:
|
||||
flash("Database connection error", "error")
|
||||
return redirect(url_for('pending_approval'))
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Get the approver name from the form or use default
|
||||
approver = request.form.get('approver', 'System')
|
||||
|
||||
# Update the transaction
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE transactions
|
||||
SET approved = TRUE,
|
||||
approved_at = CURRENT_TIMESTAMP,
|
||||
approved_by = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(approver, id)
|
||||
)
|
||||
conn.commit()
|
||||
flash(f"Transaction #{id} approved successfully", "success")
|
||||
except Exception as e:
|
||||
logger.error(f"Error approving transaction: {e}")
|
||||
flash(f"Error approving transaction: {e}", "error")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('pending_approval'))
|
||||
|
||||
@app.route('/api/transactions/pending', methods=['GET'])
|
||||
def api_get_pending_transactions():
|
||||
"""API endpoint to get all pending (unapproved) transactions"""
|
||||
conn = get_db_connection()
|
||||
if conn is None:
|
||||
return jsonify({"error": "Database connection error"}), 500
|
||||
|
||||
transactions = []
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT * FROM transactions WHERE approved = FALSE ORDER BY id DESC')
|
||||
transactions = cur.fetchall()
|
||||
|
||||
# Convert transactions to a list of dictionaries
|
||||
result = []
|
||||
for transaction in transactions:
|
||||
# Format the date if it exists
|
||||
if transaction.get('source_date'):
|
||||
transaction['source_date'] = transaction['source_date'].strftime('%Y-%m-%d')
|
||||
|
||||
# Format created_at if it exists
|
||||
if transaction.get('created_at'):
|
||||
transaction['created_at'] = transaction['created_at'].strftime('%Y-%m-%dT%H:%M:%S.%f')
|
||||
|
||||
result.append(dict(transaction))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error in API: {e}")
|
||||
return jsonify({"error": f"Database error: {str(e)}"}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@app.route('/api/transaction/<int:id>/approve', methods=['POST'])
|
||||
def api_approve_transaction(id):
|
||||
"""API endpoint to approve a transaction"""
|
||||
conn = get_db_connection()
|
||||
if conn is None:
|
||||
return jsonify({"error": "Database connection error"}), 500
|
||||
|
||||
# Get the approver from the request data or use default
|
||||
data = request.get_json() if request.is_json else {}
|
||||
approver = data.get('approver', 'API')
|
||||
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# Check if transaction exists
|
||||
cur.execute('SELECT id FROM transactions WHERE id = %s', (id,))
|
||||
if cur.fetchone() is None:
|
||||
return jsonify({"error": "Transaction not found"}), 404
|
||||
|
||||
# Update the transaction
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE transactions
|
||||
SET approved = TRUE,
|
||||
approved_at = CURRENT_TIMESTAMP,
|
||||
approved_by = %s
|
||||
WHERE id = %s
|
||||
""",
|
||||
(approver, id)
|
||||
)
|
||||
conn.commit()
|
||||
return jsonify({"message": "Transaction approved successfully"}), 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error approving transaction via API: {e}")
|
||||
return jsonify({"error": f"Error approving transaction: {str(e)}"}), 500
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@app.route('/transaction/add', methods=['GET', 'POST'])
|
||||
def create_transaction():
|
||||
if request.method == 'POST':
|
||||
|
@ -343,12 +473,13 @@ def create_transaction():
|
|||
INSERT INTO transactions (
|
||||
transaction_type, company_division, address_1, address_2,
|
||||
city, province, region, postal_code, is_primary, source_date, source_description,
|
||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments,
|
||||
approved
|
||||
) VALUES (
|
||||
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
||||
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
||||
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
||||
%(commodity_class)s, %(contract_number)s, %(comments)s
|
||||
%(commodity_class)s, %(contract_number)s, %(comments)s, FALSE
|
||||
) RETURNING id
|
||||
""",
|
||||
data
|
||||
|
@ -367,6 +498,7 @@ def create_transaction():
|
|||
finally:
|
||||
conn.close()
|
||||
|
||||
flash("Transaction created successfully and is pending approval", "success")
|
||||
return redirect(url_for('view_transaction', id=new_transaction_id))
|
||||
|
||||
return render_template('transaction_form.html', version=VERSION)
|
||||
|
@ -641,12 +773,13 @@ def api_create_transaction():
|
|||
INSERT INTO transactions (
|
||||
transaction_type, company_division, address_1, address_2,
|
||||
city, province, region, postal_code, is_primary, source_date, source_description,
|
||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments,
|
||||
approved
|
||||
) VALUES (
|
||||
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
||||
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
||||
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
||||
%(commodity_class)s, %(contract_number)s, %(comments)s
|
||||
%(commodity_class)s, %(contract_number)s, %(comments)s, FALSE
|
||||
) RETURNING id
|
||||
""",
|
||||
data
|
||||
|
@ -655,7 +788,7 @@ def api_create_transaction():
|
|||
if result and 'id' in result:
|
||||
new_transaction_id = result['id']
|
||||
conn.commit()
|
||||
return jsonify({"message": "Transaction created successfully", "transaction_id": new_transaction_id}), 201
|
||||
return jsonify({"message": "Transaction created successfully and is pending approval", "transaction_id": new_transaction_id}), 201
|
||||
else:
|
||||
return jsonify({"error": "Could not create transaction"}), 500
|
||||
except Exception as e:
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import os
|
||||
import psycopg2
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('ploughshares')
|
||||
|
||||
def get_db_connection():
|
||||
host = os.environ.get('POSTGRES_HOST', 'db')
|
||||
port = os.environ.get('POSTGRES_PORT', '5432')
|
||||
dbname = os.environ.get('POSTGRES_DB', 'ploughshares')
|
||||
user = os.environ.get('POSTGRES_USER', 'ploughshares')
|
||||
password = os.environ.get('POSTGRES_PASSWORD', 'ploughshares_password')
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
dbname=dbname,
|
||||
user=user,
|
||||
password=password
|
||||
)
|
||||
conn.autocommit = True
|
||||
return conn
|
||||
|
||||
def migrate_database():
|
||||
"""
|
||||
Adds approval fields to the transactions table if they don't exist
|
||||
"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if the approved column already exists
|
||||
cursor.execute("""
|
||||
SELECT column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'transactions' AND column_name = 'approved'
|
||||
""")
|
||||
|
||||
if cursor.fetchone() is None:
|
||||
logger.info("Adding approval fields to transactions table...")
|
||||
|
||||
# Add the new columns
|
||||
cursor.execute("""
|
||||
ALTER TABLE transactions
|
||||
ADD COLUMN IF NOT EXISTS approved BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS approved_by VARCHAR(255)
|
||||
""")
|
||||
|
||||
# Create an index on the approved column
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_approved
|
||||
ON transactions(approved)
|
||||
""")
|
||||
|
||||
logger.info("Successfully added approval fields to transactions table")
|
||||
else:
|
||||
logger.info("Approval fields already exist in transactions table")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error migrating database: {e}")
|
||||
finally:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_database()
|
|
@ -23,6 +23,9 @@ CREATE TABLE IF NOT EXISTS transactions (
|
|||
commodity_class VARCHAR(255),
|
||||
contract_number VARCHAR(255),
|
||||
comments TEXT,
|
||||
approved BOOLEAN DEFAULT FALSE,
|
||||
approved_at TIMESTAMP,
|
||||
approved_by VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
@ -43,3 +46,4 @@ CREATE INDEX IF NOT EXISTS idx_transactions_type ON transactions(transaction_typ
|
|||
CREATE INDEX IF NOT EXISTS idx_transactions_division ON transactions(company_division);
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_recipient ON transactions(recipient);
|
||||
CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(source_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_transactions_approved ON transactions(approved);
|
|
@ -147,6 +147,55 @@
|
|||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"message": "Transaction deleted successfully"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">6. Get Pending Transactions</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>GET</strong> <code>/api/transactions/pending</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="getPendingTransactions">curl -X GET "http://{{ server_name }}/api/transactions/pending"</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('getPendingTransactions')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>[
|
||||
{
|
||||
"transaction_id": 1,
|
||||
"transaction_type": "Subcontract",
|
||||
"company_division": "C A E Inc",
|
||||
"amount": 0.00,
|
||||
"recipient": "US Army",
|
||||
"created_at": "2023-07-02T12:34:56.789012",
|
||||
"approved": false
|
||||
},
|
||||
{
|
||||
"transaction_id": 2,
|
||||
"transaction_type": "Purchase Order",
|
||||
"company_division": "Example Corp",
|
||||
"amount": 1000.00,
|
||||
"recipient": "Test Recipient",
|
||||
"created_at": "2023-07-03T10:11:12.131415",
|
||||
"approved": false
|
||||
}
|
||||
]</code></pre>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">7. Approve Transaction</h4>
|
||||
<div class="bg-light p-3 mb-3">
|
||||
<p><strong>POST</strong> <code>/api/transaction/{id}/approve</code></p>
|
||||
|
||||
<h5>Complete Example:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code id="approveTransaction">curl -X POST "http://{{ server_name }}/api/transaction/2/approve" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"approver": "John Doe"
|
||||
}'</code></pre>
|
||||
<button class="btn btn-sm btn-secondary" onclick="copyToClipboard('approveTransaction')">Copy</button>
|
||||
|
||||
<h5>Response:</h5>
|
||||
<pre class="bg-dark text-light p-2"><code>{
|
||||
"message": "Transaction approved successfully"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,12 @@
|
|||
<i class="bi bi-plus-circle" aria-hidden="true"></i> New Transaction
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('pending_approval') }}" aria-current="{% if request.endpoint == 'pending_approval' %}page{% endif %}">
|
||||
<i class="bi bi-clock-history" aria-hidden="true"></i> Pending Approval
|
||||
<span class="badge bg-warning rounded-pill" id="pending-count"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('api_docs') }}" aria-current="{% if request.endpoint == 'api_docs' %}page{% endif %}">
|
||||
<i class="bi bi-file-earmark-text" aria-hidden="true"></i> API Documentation
|
||||
|
@ -105,6 +111,26 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update the pending count badge
|
||||
const pendingCountBadge = document.getElementById('pending-count');
|
||||
if (pendingCountBadge) {
|
||||
fetch('/api/transactions/pending')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const count = data.length;
|
||||
if (count > 0) {
|
||||
pendingCountBadge.textContent = count;
|
||||
pendingCountBadge.style.display = 'inline-block';
|
||||
} else {
|
||||
pendingCountBadge.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching pending transactions:', error);
|
||||
pendingCountBadge.style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
<th class="amount-cell">Amount</th>
|
||||
<th>Source Date</th>
|
||||
<th>Recipient</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -34,6 +35,13 @@
|
|||
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
||||
<td>{{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }}</td>
|
||||
<td>{{ transaction['recipient'] }}</td>
|
||||
<td>
|
||||
{% if transaction['approved'] %}
|
||||
<span class="badge bg-success">Approved</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Pending</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_transaction', id=transaction['id']) }}" class="btn btn-sm btn-info" aria-label="View transaction {{ transaction['id'] }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Transactions Pending Approval - Project Ploughshares{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2>Transactions Pending Approval</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to All Transactions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if transactions %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>Transaction No.</th>
|
||||
<th>Type</th>
|
||||
<th>Division</th>
|
||||
<th class="amount-cell">Amount</th>
|
||||
<th>Source Date</th>
|
||||
<th>Recipient</th>
|
||||
<th>Created At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in transactions %}
|
||||
<tr>
|
||||
<td>{{ transaction['id'] }}</td>
|
||||
<td>{{ transaction['transaction_type'] }}</td>
|
||||
<td>{{ transaction['company_division'] }}</td>
|
||||
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
||||
<td>{{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }}</td>
|
||||
<td>{{ transaction['recipient'] }}</td>
|
||||
<td>{{ transaction['created_at'].strftime('%Y-%m-%d %H:%M') if transaction['created_at'] else 'N/A' }}</td>
|
||||
<td>
|
||||
<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'] }}">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<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>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#approveModal{{ transaction['id'] }}" aria-label="Approve transaction {{ transaction['id'] }}">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Approve Modal -->
|
||||
<div class="modal fade" id="approveModal{{ transaction['id'] }}" tabindex="-1" aria-labelledby="approveModalLabel{{ transaction['id'] }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="approveModalLabel{{ transaction['id'] }}">Approve Transaction #{{ transaction['id'] }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for('approve_transaction', id=transaction['id']) }}" method="post">
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to approve this transaction?</p>
|
||||
<div class="mb-3">
|
||||
<label for="approver{{ transaction['id'] }}" class="form-label">Your Name</label>
|
||||
<input type="text" class="form-control" id="approver{{ transaction['id'] }}" name="approver" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success">Approve</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<p>No transactions pending approval.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script nonce="{{ csp_nonce() }}">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add any specific scripts for the pending approval page
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -17,6 +17,49 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Approval status banner -->
|
||||
{% if transaction['approved'] %}
|
||||
<div class="alert alert-success mb-3">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
Approved by {{ transaction['approved_by'] }} on {{ transaction['approved_at'].strftime('%Y-%m-%d %H:%M') if transaction['approved_at'] else 'N/A' }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mb-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
Pending Approval
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal">
|
||||
<i class="bi bi-check-lg"></i> Approve
|
||||
</button>
|
||||
|
||||
<!-- Approve Modal -->
|
||||
<div class="modal fade" id="approveModal" tabindex="-1" aria-labelledby="approveModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="approveModalLabel">Approve Transaction #{{ transaction['id'] }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{{ url_for('approve_transaction', id=transaction['id']) }}" method="post">
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to approve this transaction?</p>
|
||||
<div class="mb-3">
|
||||
<label for="approver" class="form-label">Your Name</label>
|
||||
<input type="text" class="form-control" id="approver" name="approver" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-success">Approve</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
{% if prev_id %}
|
||||
|
|
|
@ -2,3 +2,4 @@ Thu Jul 3 13:33:04 EDT 2025: Version changed from 0.1.0 to 0.1.1
|
|||
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 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
|
||||
|
|
Loading…
Reference in New Issue