feat: Enhance governance dashboard with activity tracking
- Add governance activity tracker to record user actions. - Display recent activities on the governance dashboard. - Add a dedicated page to view all governance activities. - Improve header information and styling across governance pages. - Track proposal creation and voting activities.
This commit is contained in:
18
actix_mvc_app/src/views/governance/_header.html
Normal file
18
actix_mvc_app/src/views/governance/_header.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- Governance Page Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">{{ page_title }}</h1>
|
||||
<p class="text-muted mb-0">{{ page_description }}</p>
|
||||
</div>
|
||||
{% if show_create_button %}
|
||||
<div>
|
||||
<a href="/governance/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!-- Governance Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'dashboard' %}active{% endif %}" href="/governance">
|
||||
<i class="bi bi-house"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'proposals' %}active{% endif %}" href="/governance/proposals">
|
||||
<i class="bi bi-file-text"></i> All Proposals
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'create' %}active{% endif %}" href="/governance/create">
|
||||
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'my-votes' %}active{% endif %}" href="/governance/my-votes">
|
||||
<i class="bi bi-check-circle"></i> My Votes
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_tab == 'activities' %}active{% endif %}" href="/governance/activities">
|
||||
<i class="bi bi-activity"></i> All Activities
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
@@ -0,0 +1,118 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}All Governance Activities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Activities List -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-activity"></i> Governance Activity History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if activities %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">Type</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Proposal</th>
|
||||
<th width="150">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for activity in activities %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="{{ activity.icon }}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ activity.user }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ activity.action }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/governance/proposals/{{ activity.proposal_id }}"
|
||||
class="text-decoration-none">
|
||||
{{ activity.proposal_title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ activity.timestamp | date(format="%Y-%m-%d %H:%M") }}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-activity display-1 text-muted"></i>
|
||||
<h4 class="mt-3">No Activities Yet</h4>
|
||||
<p class="text-muted">
|
||||
Governance activities will appear here as users create proposals and cast votes.
|
||||
</p>
|
||||
<a href="/governance/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First Proposal
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Statistics -->
|
||||
{% if activities %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ activities | length }}</h5>
|
||||
<p class="card-text text-muted">Total Activities</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-activity text-primary"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Activity Timeline</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-people text-success"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Community Engagement</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -4,25 +4,11 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row">
|
||||
|
@@ -3,25 +3,11 @@
|
||||
{% block title %}Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row mb-2">
|
||||
@@ -159,7 +145,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
|
||||
<a href="/governance/activities" class="btn btn-sm btn-outline-info">View All Activities</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -3,25 +3,11 @@
|
||||
{% block title %}My Votes - Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Info Alert -->
|
||||
<div class="row">
|
||||
|
@@ -35,6 +35,12 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
@@ -194,7 +200,8 @@
|
||||
{% if proposal.status == "Active" and user and user.id %}
|
||||
<div class="mt-auto">
|
||||
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
|
||||
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
|
||||
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post"
|
||||
id="voteForm">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<div class="form-check">
|
||||
@@ -243,26 +250,8 @@
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap">
|
||||
<h5 class="mb-0 mb-md-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="input-group input-group-sm me-2 d-none d-md-flex" style="width: 200px;">
|
||||
<span class="input-group-text bg-white">
|
||||
<i class="bi bi-search"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control border-start-0" id="voteSearch"
|
||||
placeholder="Search votes...">
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="Filter votes">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary active"
|
||||
data-filter="all">All</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success"
|
||||
data-filter="yes">Yes</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-filter="no">No</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||
data-filter="abstain">Abstain</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
@@ -372,275 +361,255 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Remove query parameters from URL without refreshing the page
|
||||
if (window.location.search.includes('vote_success=true')) {
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Remove query parameters from URL without refreshing the page
|
||||
if (window.location.search.includes('vote_success=true')) {
|
||||
const newUrl = window.location.pathname;
|
||||
window.history.replaceState({}, document.title, newUrl);
|
||||
|
||||
// Auto-hide the success alert after 5 seconds
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
// Auto-hide the success alert after 5 seconds
|
||||
const successAlert = document.querySelector('.alert-success');
|
||||
if (successAlert) {
|
||||
setTimeout(function () {
|
||||
successAlert.classList.remove('show');
|
||||
setTimeout(function () {
|
||||
successAlert.classList.remove('show');
|
||||
setTimeout(function () {
|
||||
successAlert.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
successAlert.remove();
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination functionality
|
||||
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
||||
const paginationControls = document.getElementById('paginationControls');
|
||||
const votesTableBody = document.getElementById('votesTableBody');
|
||||
const startRowElement = document.getElementById('startRow');
|
||||
const endRowElement = document.getElementById('endRow');
|
||||
const totalRowsElement = document.getElementById('totalRows');
|
||||
const prevPageBtn = document.getElementById('prevPage');
|
||||
const nextPageBtn = document.getElementById('nextPage');
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
|
||||
|
||||
// Function to update pagination display
|
||||
function updatePagination() {
|
||||
if (!paginationControls) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Vote filtering using data-filter attributes
|
||||
const filterButtons = document.querySelectorAll('[data-filter]');
|
||||
const voteRows = document.querySelectorAll('.vote-row');
|
||||
const searchInput = document.getElementById('voteSearch');
|
||||
|
||||
// Filter votes by type
|
||||
filterButtons.forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
// Update active button
|
||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Reset to first page and update pagination
|
||||
currentPage = 1;
|
||||
updatePagination();
|
||||
});
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function () {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
|
||||
voteRows.forEach(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
|
||||
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Reset pagination after search
|
||||
currentPage = 1;
|
||||
updatePagination();
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
// Pagination functionality
|
||||
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
||||
const paginationControls = document.getElementById('paginationControls');
|
||||
const votesTableBody = document.getElementById('votesTableBody');
|
||||
const startRowElement = document.getElementById('startRow');
|
||||
const endRowElement = document.getElementById('endRow');
|
||||
const totalRowsElement = document.getElementById('totalRows');
|
||||
const prevPageBtn = document.getElementById('prevPage');
|
||||
const nextPageBtn = document.getElementById('nextPage');
|
||||
const totalRows = filteredRows.length;
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
|
||||
// Calculate total pages
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Function to update pagination display
|
||||
function updatePagination() {
|
||||
if (!paginationControls) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = filteredRows.length;
|
||||
|
||||
// Calculate total pages
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Update pagination controls
|
||||
if (paginationControls) {
|
||||
// Clear existing page links (except prev/next)
|
||||
const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
|
||||
pageLinks.forEach(link => link.remove());
|
||||
|
||||
// Add new page links
|
||||
const maxVisiblePages = 5;
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// Adjust if we're near the end
|
||||
if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
// Insert page links before the next button
|
||||
const nextPageElement = document.getElementById('nextPage');
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.className = 'page-link';
|
||||
a.href = '#';
|
||||
a.textContent = i;
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
currentPage = i;
|
||||
updatePagination();
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
paginationControls.insertBefore(li, nextPageElement);
|
||||
}
|
||||
|
||||
// Update prev/next buttons
|
||||
prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||
nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||
}
|
||||
|
||||
// Show current page
|
||||
showCurrentPage();
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Function to show current page
|
||||
function showCurrentPage() {
|
||||
if (!votesTableBody) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide all rows first
|
||||
voteRows.forEach(row => row.style.display = 'none');
|
||||
|
||||
// Calculate pagination
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Show only rows for current page
|
||||
const start = (currentPage - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
filteredRows.slice(start, end).forEach(row => row.style.display = '');
|
||||
|
||||
// Update pagination info
|
||||
if (startRowElement && endRowElement && totalRowsElement) {
|
||||
startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
|
||||
endRowElement.textContent = Math.min(end, totalRows);
|
||||
totalRowsElement.textContent = totalRows;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for pagination
|
||||
if (prevPageBtn) {
|
||||
prevPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextPageBtn) {
|
||||
nextPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (rowsPerPageSelect) {
|
||||
rowsPerPageSelect.addEventListener('change', function () {
|
||||
rowsPerPage = parseInt(this.value);
|
||||
currentPage = 1; // Reset to first page
|
||||
updatePagination();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize pagination
|
||||
// Update pagination controls
|
||||
if (paginationControls) {
|
||||
updatePagination();
|
||||
// Clear existing page links (except prev/next)
|
||||
const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
|
||||
pageLinks.forEach(link => link.remove());
|
||||
|
||||
// Add new page links
|
||||
const maxVisiblePages = 5;
|
||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||
|
||||
// Adjust if we're near the end
|
||||
if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
|
||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||
}
|
||||
|
||||
// Insert page links before the next button
|
||||
const nextPageElement = document.getElementById('nextPage');
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.className = 'page-link';
|
||||
a.href = '#';
|
||||
a.textContent = i;
|
||||
a.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
currentPage = i;
|
||||
updatePagination();
|
||||
});
|
||||
|
||||
li.appendChild(a);
|
||||
paginationControls.insertBefore(li, nextPageElement);
|
||||
}
|
||||
|
||||
// Update prev/next buttons
|
||||
prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||
nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||
}
|
||||
|
||||
// Initialize tooltips for all elements with title attributes
|
||||
const tooltipElements = document.querySelectorAll('[title]');
|
||||
if (tooltipElements.length > 0) {
|
||||
[].slice.call(tooltipElements).map(function (el) {
|
||||
return new bootstrap.Tooltip(el);
|
||||
// Show current page
|
||||
showCurrentPage();
|
||||
}
|
||||
|
||||
// Function to show current page
|
||||
function showCurrentPage() {
|
||||
if (!votesTableBody) return;
|
||||
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
||||
{% endblock content %}
|
||||
// Hide all rows first
|
||||
voteRows.forEach(row => row.style.display = 'none');
|
||||
|
||||
// Calculate pagination
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
// Ensure current page is valid
|
||||
if (currentPage > totalPages) {
|
||||
currentPage = totalPages;
|
||||
}
|
||||
|
||||
// Show only rows for current page
|
||||
const start = (currentPage - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
filteredRows.slice(start, end).forEach(row => row.style.display = '');
|
||||
|
||||
// Update pagination info
|
||||
if (startRowElement && endRowElement && totalRowsElement) {
|
||||
startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
|
||||
endRowElement.textContent = Math.min(end, totalRows);
|
||||
totalRowsElement.textContent = totalRows;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for pagination
|
||||
if (prevPageBtn) {
|
||||
prevPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextPageBtn) {
|
||||
nextPageBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
// Get all rows that match the current filter
|
||||
const currentFilter = document.querySelector('[data-filter].active');
|
||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||
|
||||
// Get rows that match the current filter and search term
|
||||
let filteredRows = Array.from(voteRows);
|
||||
if (filterType !== 'all') {
|
||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||
}
|
||||
|
||||
// Apply search filter if there's a search term
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
if (searchTerm) {
|
||||
filteredRows = filteredRows.filter(row => {
|
||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
const totalRows = filteredRows.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (rowsPerPageSelect) {
|
||||
rowsPerPageSelect.addEventListener('change', function () {
|
||||
rowsPerPage = parseInt(this.value);
|
||||
currentPage = 1; // Reset to first page
|
||||
updatePagination();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize pagination (but don't interfere with filtering)
|
||||
if (paginationControls) {
|
||||
// Only initialize pagination if there are many votes
|
||||
// The filtering will handle showing/hiding rows
|
||||
console.log('Pagination controls available but not interfering with filtering');
|
||||
}
|
||||
|
||||
// Initialize tooltips for all elements with title attributes
|
||||
const tooltipElements = document.querySelectorAll('[title]');
|
||||
if (tooltipElements.length > 0) {
|
||||
[].slice.call(tooltipElements).map(function (el) {
|
||||
return new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
|
||||
// Add debugging for vote form
|
||||
const voteForm = document.getElementById('voteForm');
|
||||
if (voteForm) {
|
||||
console.log('Vote form found:', voteForm);
|
||||
voteForm.addEventListener('submit', function (e) {
|
||||
console.log('Vote form submitted');
|
||||
const formData = new FormData(voteForm);
|
||||
console.log('Form data:', Object.fromEntries(formData));
|
||||
});
|
||||
} else {
|
||||
console.log('Vote form not found');
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log('Filter buttons found:', filterButtons.length);
|
||||
console.log('Vote rows found:', voteRows.length);
|
||||
console.log('Search input found:', searchInput ? 'Yes' : 'No');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@@ -3,6 +3,12 @@
|
||||
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Header -->
|
||||
{% include "governance/_header.html" %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
{% include "governance/_tabs.html" %}
|
||||
|
||||
<!-- Success message if present -->
|
||||
{% if success %}
|
||||
<div class="row mb-4">
|
||||
@@ -15,26 +21,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/governance/proposals">All Proposals</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@@ -58,18 +44,23 @@
|
||||
<div class="col-md-4">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="" {% if not status_filter or status_filter == "" %}selected{% endif %}>All Statuses</option>
|
||||
<option value="Draft" {% if status_filter == "Draft" %}selected{% endif %}>Draft</option>
|
||||
<option value="Active" {% if status_filter == "Active" %}selected{% endif %}>Active</option>
|
||||
<option value="Approved" {% if status_filter == "Approved" %}selected{% endif %}>Approved</option>
|
||||
<option value="Rejected" {% if status_filter == "Rejected" %}selected{% endif %}>Rejected</option>
|
||||
<option value="Cancelled" {% if status_filter == "Cancelled" %}selected{% endif %}>Cancelled</option>
|
||||
<option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
|
||||
Statuses</option>
|
||||
<option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
|
||||
<option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
|
||||
<option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
|
||||
</option>
|
||||
<option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
|
||||
</option>
|
||||
<option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="Search by title or description" value="{% if search_filter %}{{ search_filter }}{% endif %}">
|
||||
placeholder="Search by title or description"
|
||||
value="{% if search_filter %}{{ search_filter }}{% endif %}">
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
||||
@@ -130,21 +121,22 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center py-5">
|
||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||
<h5>No proposals found</h5>
|
||||
{% if status_filter or search_filter %}
|
||||
<p>No proposals match your current filter criteria. Try adjusting your filters or <a href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
|
||||
{% else %}
|
||||
<p>There are no proposals in the system yet.</p>
|
||||
<div class="alert alert-info text-center py-5">
|
||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||
<h5>No proposals found</h5>
|
||||
{% if status_filter or search_filter %}
|
||||
<p>No proposals match your current filter criteria. Try adjusting your filters or <a
|
||||
href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
|
||||
{% else %}
|
||||
<p>There are no proposals in the system yet.</p>
|
||||
{% endif %}
|
||||
<a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user