feat: Enhance Governance Controller and Proposal Handling
- Improve proposal search to include description field: This allows for more comprehensive search results. - Fix redirect after voting: The redirect now correctly handles the success message. - Handle potential invalid timestamps in ballots: The code now gracefully handles ballots with invalid timestamps, preventing crashes and using the current time as a fallback. - Add local time formatting function: This provides a way to display dates and times in the user's local timezone. - Update database path: This simplifies the database setup. - Improve proposal vote handling: Addresses issues with vote submission and timestamping. - Add client-side pagination and filtering to proposal details: Improves user experience for viewing large vote lists.
This commit is contained in:
parent
97e7a04827
commit
d12a082ca1
@ -135,8 +135,8 @@ impl GovernanceController {
|
|||||||
/// Handles the proposal list page route
|
/// Handles the proposal list page route
|
||||||
pub async fn proposals(
|
pub async fn proposals(
|
||||||
query: web::Query<ProposalQuery>,
|
query: web::Query<ProposalQuery>,
|
||||||
tmpl: web::Data<Tera>,
|
tmpl: web::Data<Tera>,
|
||||||
session: Session
|
session: Session,
|
||||||
) -> Result<impl Responder> {
|
) -> Result<impl Responder> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
@ -176,8 +176,8 @@ impl GovernanceController {
|
|||||||
proposals = proposals
|
proposals = proposals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| {
|
.filter(|p| {
|
||||||
p.title.to_lowercase().contains(&search_term) ||
|
p.title.to_lowercase().contains(&search_term)
|
||||||
p.description.to_lowercase().contains(&search_term)
|
|| p.description.to_lowercase().contains(&search_term)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
@ -185,7 +185,7 @@ impl GovernanceController {
|
|||||||
|
|
||||||
// Add the filtered proposals to the context
|
// Add the filtered proposals to the context
|
||||||
ctx.insert("proposals", &proposals);
|
ctx.insert("proposals", &proposals);
|
||||||
|
|
||||||
// Add the filter values back to the context for form persistence
|
// Add the filter values back to the context for form persistence
|
||||||
ctx.insert("status_filter", &query.status);
|
ctx.insert("status_filter", &query.status);
|
||||||
ctx.insert("search_filter", &query.search);
|
ctx.insert("search_filter", &query.search);
|
||||||
@ -224,7 +224,7 @@ impl GovernanceController {
|
|||||||
// Calculate voting results directly from the proposal
|
// Calculate voting results directly from the proposal
|
||||||
let results = Self::calculate_voting_results_from_proposal(&proposal);
|
let results = Self::calculate_voting_results_from_proposal(&proposal);
|
||||||
ctx.insert("results", &results);
|
ctx.insert("results", &results);
|
||||||
|
|
||||||
// Check if vote_success parameter is present and add success message
|
// Check if vote_success parameter is present and add success message
|
||||||
if vote_success {
|
if vote_success {
|
||||||
ctx.insert("success", "Your vote has been successfully recorded!");
|
ctx.insert("success", "Your vote has been successfully recorded!");
|
||||||
@ -349,7 +349,7 @@ impl GovernanceController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ctx.insert("proposals", &proposals);
|
ctx.insert("proposals", &proposals);
|
||||||
|
|
||||||
// Add the required context variables for the proposals template
|
// Add the required context variables for the proposals template
|
||||||
ctx.insert("active_tab", "proposals");
|
ctx.insert("active_tab", "proposals");
|
||||||
ctx.insert("status_filter", &None::<String>);
|
ctx.insert("status_filter", &None::<String>);
|
||||||
@ -400,10 +400,13 @@ impl GovernanceController {
|
|||||||
1, // Default to 1 share
|
1, // Default to 1 share
|
||||||
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
||||||
) {
|
) {
|
||||||
Ok(updated_proposal) => {
|
Ok(_) => {
|
||||||
// Redirect to the proposal detail page with a success message
|
// Redirect to the proposal detail page with a success message
|
||||||
return Ok(HttpResponse::Found()
|
return Ok(HttpResponse::Found()
|
||||||
.append_header(("Location", format!("/governance/proposals/{}?vote_success=true", proposal_id)))
|
.append_header((
|
||||||
|
"Location",
|
||||||
|
format!("/governance/proposals/{}?vote_success=true", proposal_id),
|
||||||
|
))
|
||||||
.finish());
|
.finish());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -454,7 +457,7 @@ impl GovernanceController {
|
|||||||
ctx.insert("total_yes_votes", &total_vote_counts.0);
|
ctx.insert("total_yes_votes", &total_vote_counts.0);
|
||||||
ctx.insert("total_no_votes", &total_vote_counts.1);
|
ctx.insert("total_no_votes", &total_vote_counts.1);
|
||||||
ctx.insert("total_abstain_votes", &total_vote_counts.2);
|
ctx.insert("total_abstain_votes", &total_vote_counts.2);
|
||||||
|
|
||||||
ctx.insert("votes", &user_votes);
|
ctx.insert("votes", &user_votes);
|
||||||
|
|
||||||
render_template(&tmpl, "governance/my_votes.html", &ctx)
|
render_template(&tmpl, "governance/my_votes.html", &ctx)
|
||||||
@ -602,19 +605,31 @@ impl GovernanceController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a Vote from the ballot
|
let ballot_timestamp =
|
||||||
let vote = Vote::new(
|
match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) {
|
||||||
proposal.base_data.id.to_string(),
|
Some(dt) => dt,
|
||||||
|
None => {
|
||||||
|
println!(
|
||||||
|
"Warning: Invalid timestamp {} for ballot, using current time",
|
||||||
|
ballot.base_data.created_at
|
||||||
|
);
|
||||||
|
Utc::now()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let vote = Vote {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
proposal_id: proposal.base_data.id.to_string(),
|
||||||
voter_id,
|
voter_id,
|
||||||
format!("User {}", voter_id),
|
voter_name: format!("User {}", voter_id),
|
||||||
vote_type,
|
vote_type,
|
||||||
ballot.comment.clone(), // Use the comment from the ballot
|
comment: ballot.comment.clone(),
|
||||||
);
|
created_at: ballot_timestamp, // This is already local time
|
||||||
|
updated_at: ballot_timestamp, // Same as created_at for votes
|
||||||
|
};
|
||||||
|
|
||||||
votes.push(vote);
|
votes.push(vote);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Extracted {} votes from proposal", votes.len());
|
|
||||||
votes
|
votes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use heromodels::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// The path to the database file. Change this as needed for your environment.
|
/// The path to the database file. Change this as needed for your environment.
|
||||||
pub const DB_PATH: &str = "/tmp/ourdb_governance6";
|
pub const DB_PATH: &str = "/tmp/ourdb_governance";
|
||||||
|
|
||||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
|
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
|
||||||
pub fn get_db(db_path: &str) -> Result<OurDB, String> {
|
pub fn get_db(db_path: &str) -> Result<OurDB, String> {
|
||||||
@ -188,6 +188,15 @@ pub fn submit_vote_on_proposal(
|
|||||||
// Set the comment if provided
|
// Set the comment if provided
|
||||||
ballot.comment = comment;
|
ballot.comment = comment;
|
||||||
|
|
||||||
|
// Store the local time (EEST = UTC+3) as the vote timestamp
|
||||||
|
// This ensures the displayed time matches the user's local time
|
||||||
|
let utc_now = Utc::now();
|
||||||
|
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||||
|
let local_time = utc_now.with_timezone(&local_offset);
|
||||||
|
|
||||||
|
// Store the local time as a timestamp (this is what will be displayed)
|
||||||
|
ballot.base_data.created_at = local_time.timestamp();
|
||||||
|
|
||||||
// Add the ballot to the proposal's ballots
|
// Add the ballot to the proposal's ballots
|
||||||
proposal.ballots.push(ballot);
|
proposal.ballots.push(ballot);
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ impl std::error::Error for TemplateError {}
|
|||||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||||
tera.register_function("now", NowFunction);
|
tera.register_function("now", NowFunction);
|
||||||
tera.register_function("format_date", FormatDateFunction);
|
tera.register_function("format_date", FormatDateFunction);
|
||||||
|
tera.register_function("local_time", LocalTimeFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tera function to get the current date/time
|
/// Tera function to get the current date/time
|
||||||
@ -93,6 +94,52 @@ impl Function for FormatDateFunction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tera function to convert UTC datetime to local time
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LocalTimeFunction;
|
||||||
|
|
||||||
|
impl Function for LocalTimeFunction {
|
||||||
|
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
|
||||||
|
let datetime_value = match args.get("datetime") {
|
||||||
|
Some(val) => val,
|
||||||
|
None => return Err(tera::Error::msg("The 'datetime' argument is required")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let format = match args.get("format") {
|
||||||
|
Some(val) => match val.as_str() {
|
||||||
|
Some(s) => s,
|
||||||
|
None => "%Y-%m-%d %H:%M",
|
||||||
|
},
|
||||||
|
None => "%Y-%m-%d %H:%M",
|
||||||
|
};
|
||||||
|
|
||||||
|
// The datetime comes from Rust as a serialized DateTime<Utc>
|
||||||
|
// We need to handle it properly
|
||||||
|
let utc_datetime = if let Some(dt_str) = datetime_value.as_str() {
|
||||||
|
// Try to parse as RFC3339 first
|
||||||
|
match DateTime::parse_from_rfc3339(dt_str) {
|
||||||
|
Ok(dt) => dt.with_timezone(&Utc),
|
||||||
|
Err(_) => {
|
||||||
|
// Try to parse as our standard format
|
||||||
|
match DateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
|
||||||
|
Ok(dt) => dt.with_timezone(&Utc),
|
||||||
|
Err(_) => return Err(tera::Error::msg("Invalid datetime string format")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(tera::Error::msg("Datetime must be a string"));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert UTC to local time (EEST = UTC+3)
|
||||||
|
// In a real application, you'd want to get the user's timezone from their profile
|
||||||
|
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
|
||||||
|
let local_datetime = utc_datetime.with_timezone(&local_offset);
|
||||||
|
|
||||||
|
Ok(Value::String(local_datetime.format(format).to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats a date for display
|
/// Formats a date for display
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
|
||||||
|
@ -329,7 +329,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<!-- Pagination Controls -->
|
||||||
{% if votes | length > 10 %}
|
{% if votes | length > 10 %}
|
||||||
<div class="d-flex justify-content-between align-items-center p-3 border-top">
|
<div class="d-flex justify-content-between align-items-center p-3 border-top">
|
||||||
@ -362,7 +362,8 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small" id="paginationInfo">
|
<div class="text-muted small" id="paginationInfo">
|
||||||
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span id="totalRows">{{ votes | length }}</span>
|
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span
|
||||||
|
id="totalRows">{{ votes | length }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -379,19 +380,19 @@
|
|||||||
if (window.location.search.includes('vote_success=true')) {
|
if (window.location.search.includes('vote_success=true')) {
|
||||||
const newUrl = window.location.pathname;
|
const newUrl = window.location.pathname;
|
||||||
window.history.replaceState({}, document.title, newUrl);
|
window.history.replaceState({}, document.title, newUrl);
|
||||||
|
|
||||||
// Auto-hide the success alert after 5 seconds
|
// Auto-hide the success alert after 5 seconds
|
||||||
const successAlert = document.querySelector('.alert-success');
|
const successAlert = document.querySelector('.alert-success');
|
||||||
if (successAlert) {
|
if (successAlert) {
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
successAlert.classList.remove('show');
|
successAlert.classList.remove('show');
|
||||||
setTimeout(function() {
|
setTimeout(function () {
|
||||||
successAlert.remove();
|
successAlert.remove();
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vote filtering using data-filter attributes
|
// Vote filtering using data-filter attributes
|
||||||
const filterButtons = document.querySelectorAll('[data-filter]');
|
const filterButtons = document.querySelectorAll('[data-filter]');
|
||||||
const voteRows = document.querySelectorAll('.vote-row');
|
const voteRows = document.querySelectorAll('.vote-row');
|
||||||
@ -399,11 +400,11 @@
|
|||||||
|
|
||||||
// Filter votes by type
|
// Filter votes by type
|
||||||
filterButtons.forEach(button => {
|
filterButtons.forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function () {
|
||||||
// Update active button
|
// Update active button
|
||||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
this.classList.add('active');
|
this.classList.add('active');
|
||||||
|
|
||||||
// Reset to first page and update pagination
|
// Reset to first page and update pagination
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
updatePagination();
|
updatePagination();
|
||||||
@ -412,26 +413,26 @@
|
|||||||
|
|
||||||
// Search functionality
|
// Search functionality
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
searchInput.addEventListener('input', function() {
|
searchInput.addEventListener('input', function () {
|
||||||
const searchTerm = this.value.toLowerCase();
|
const searchTerm = this.value.toLowerCase();
|
||||||
|
|
||||||
voteRows.forEach(row => {
|
voteRows.forEach(row => {
|
||||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
||||||
|
|
||||||
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
|
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
|
||||||
row.style.display = '';
|
row.style.display = '';
|
||||||
} else {
|
} else {
|
||||||
row.style.display = 'none';
|
row.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset pagination after search
|
// Reset pagination after search
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
updatePagination();
|
updatePagination();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination functionality
|
// Pagination functionality
|
||||||
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
||||||
const paginationControls = document.getElementById('paginationControls');
|
const paginationControls = document.getElementById('paginationControls');
|
||||||
@ -441,24 +442,24 @@
|
|||||||
const totalRowsElement = document.getElementById('totalRows');
|
const totalRowsElement = document.getElementById('totalRows');
|
||||||
const prevPageBtn = document.getElementById('prevPage');
|
const prevPageBtn = document.getElementById('prevPage');
|
||||||
const nextPageBtn = document.getElementById('nextPage');
|
const nextPageBtn = document.getElementById('nextPage');
|
||||||
|
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
|
let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
|
||||||
|
|
||||||
// Function to update pagination display
|
// Function to update pagination display
|
||||||
function updatePagination() {
|
function updatePagination() {
|
||||||
if (!paginationControls) return;
|
if (!paginationControls) return;
|
||||||
|
|
||||||
// Get all rows that match the current filter
|
// Get all rows that match the current filter
|
||||||
const currentFilter = document.querySelector('[data-filter].active');
|
const currentFilter = document.querySelector('[data-filter].active');
|
||||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||||
|
|
||||||
// Get rows that match the current filter and search term
|
// Get rows that match the current filter and search term
|
||||||
let filteredRows = Array.from(voteRows);
|
let filteredRows = Array.from(voteRows);
|
||||||
if (filterType !== 'all') {
|
if (filterType !== 'all') {
|
||||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter if there's a search term
|
// Apply search filter if there's a search term
|
||||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
@ -468,76 +469,76 @@
|
|||||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalRows = filteredRows.length;
|
const totalRows = filteredRows.length;
|
||||||
|
|
||||||
// Calculate total pages
|
// Calculate total pages
|
||||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||||
|
|
||||||
// Ensure current page is valid
|
// Ensure current page is valid
|
||||||
if (currentPage > totalPages) {
|
if (currentPage > totalPages) {
|
||||||
currentPage = totalPages;
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update pagination controls
|
// Update pagination controls
|
||||||
if (paginationControls) {
|
if (paginationControls) {
|
||||||
// Clear existing page links (except prev/next)
|
// Clear existing page links (except prev/next)
|
||||||
const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
|
const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
|
||||||
pageLinks.forEach(link => link.remove());
|
pageLinks.forEach(link => link.remove());
|
||||||
|
|
||||||
// Add new page links
|
// Add new page links
|
||||||
const maxVisiblePages = 5;
|
const maxVisiblePages = 5;
|
||||||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||||||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||||||
|
|
||||||
// Adjust if we're near the end
|
// Adjust if we're near the end
|
||||||
if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
|
if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
|
||||||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert page links before the next button
|
// Insert page links before the next button
|
||||||
const nextPageElement = document.getElementById('nextPage');
|
const nextPageElement = document.getElementById('nextPage');
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.className = 'page-link';
|
a.className = 'page-link';
|
||||||
a.href = '#';
|
a.href = '#';
|
||||||
a.textContent = i;
|
a.textContent = i;
|
||||||
a.addEventListener('click', function(e) {
|
a.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
currentPage = i;
|
currentPage = i;
|
||||||
updatePagination();
|
updatePagination();
|
||||||
});
|
});
|
||||||
|
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
paginationControls.insertBefore(li, nextPageElement);
|
paginationControls.insertBefore(li, nextPageElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update prev/next buttons
|
// Update prev/next buttons
|
||||||
prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||||
nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show current page
|
// Show current page
|
||||||
showCurrentPage();
|
showCurrentPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to show current page
|
// Function to show current page
|
||||||
function showCurrentPage() {
|
function showCurrentPage() {
|
||||||
if (!votesTableBody) return;
|
if (!votesTableBody) return;
|
||||||
|
|
||||||
// Get all rows that match the current filter
|
// Get all rows that match the current filter
|
||||||
const currentFilter = document.querySelector('[data-filter].active');
|
const currentFilter = document.querySelector('[data-filter].active');
|
||||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||||
|
|
||||||
// Get rows that match the current filter and search term
|
// Get rows that match the current filter and search term
|
||||||
let filteredRows = Array.from(voteRows);
|
let filteredRows = Array.from(voteRows);
|
||||||
if (filterType !== 'all') {
|
if (filterType !== 'all') {
|
||||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter if there's a search term
|
// Apply search filter if there's a search term
|
||||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
@ -547,25 +548,25 @@
|
|||||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide all rows first
|
// Hide all rows first
|
||||||
voteRows.forEach(row => row.style.display = 'none');
|
voteRows.forEach(row => row.style.display = 'none');
|
||||||
|
|
||||||
// Calculate pagination
|
// Calculate pagination
|
||||||
const totalRows = filteredRows.length;
|
const totalRows = filteredRows.length;
|
||||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||||
|
|
||||||
// Ensure current page is valid
|
// Ensure current page is valid
|
||||||
if (currentPage > totalPages) {
|
if (currentPage > totalPages) {
|
||||||
currentPage = totalPages;
|
currentPage = totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show only rows for current page
|
// Show only rows for current page
|
||||||
const start = (currentPage - 1) * rowsPerPage;
|
const start = (currentPage - 1) * rowsPerPage;
|
||||||
const end = start + rowsPerPage;
|
const end = start + rowsPerPage;
|
||||||
|
|
||||||
filteredRows.slice(start, end).forEach(row => row.style.display = '');
|
filteredRows.slice(start, end).forEach(row => row.style.display = '');
|
||||||
|
|
||||||
// Update pagination info
|
// Update pagination info
|
||||||
if (startRowElement && endRowElement && totalRowsElement) {
|
if (startRowElement && endRowElement && totalRowsElement) {
|
||||||
startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
|
startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
|
||||||
@ -573,10 +574,10 @@
|
|||||||
totalRowsElement.textContent = totalRows;
|
totalRowsElement.textContent = totalRows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners for pagination
|
// Event listeners for pagination
|
||||||
if (prevPageBtn) {
|
if (prevPageBtn) {
|
||||||
prevPageBtn.addEventListener('click', function(e) {
|
prevPageBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
@ -584,20 +585,20 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPageBtn) {
|
if (nextPageBtn) {
|
||||||
nextPageBtn.addEventListener('click', function(e) {
|
nextPageBtn.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Get all rows that match the current filter
|
// Get all rows that match the current filter
|
||||||
const currentFilter = document.querySelector('[data-filter].active');
|
const currentFilter = document.querySelector('[data-filter].active');
|
||||||
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
|
||||||
|
|
||||||
// Get rows that match the current filter and search term
|
// Get rows that match the current filter and search term
|
||||||
let filteredRows = Array.from(voteRows);
|
let filteredRows = Array.from(voteRows);
|
||||||
if (filterType !== 'all') {
|
if (filterType !== 'all') {
|
||||||
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search filter if there's a search term
|
// Apply search filter if there's a search term
|
||||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
@ -607,25 +608,25 @@
|
|||||||
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
return voterName.includes(searchTerm) || comment.includes(searchTerm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalRows = filteredRows.length;
|
const totalRows = filteredRows.length;
|
||||||
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
|
||||||
|
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
updatePagination();
|
updatePagination();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowsPerPageSelect) {
|
if (rowsPerPageSelect) {
|
||||||
rowsPerPageSelect.addEventListener('change', function() {
|
rowsPerPageSelect.addEventListener('change', function () {
|
||||||
rowsPerPage = parseInt(this.value);
|
rowsPerPage = parseInt(this.value);
|
||||||
currentPage = 1; // Reset to first page
|
currentPage = 1; // Reset to first page
|
||||||
updatePagination();
|
updatePagination();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pagination
|
// Initialize pagination
|
||||||
if (paginationControls) {
|
if (paginationControls) {
|
||||||
updatePagination();
|
updatePagination();
|
||||||
|
Loading…
Reference in New Issue
Block a user