use crate::db::governance::{ self, create_activity, get_all_activities, get_proposal_by_id, get_proposals, get_recent_activities, }; // Note: Now using heromodels directly instead of local governance models use crate::utils::render_template; use actix_session::Session; use actix_web::{HttpResponse, Responder, Result, web}; use chrono::{Duration, Utc}; use heromodels::models::ActivityType; use heromodels::models::governance::{Proposal, ProposalStatus}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tera::Tera; use chrono::prelude::*; /// Simple vote type for UI display #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum VoteType { Yes, No, Abstain, } /// Simple vote structure for UI display #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Vote { pub id: String, pub proposal_id: String, pub voter_id: i32, pub voter_name: String, pub vote_type: VoteType, pub comment: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl Vote { pub fn new( proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option, ) -> Self { let now = Utc::now(); Self { id: uuid::Uuid::new_v4().to_string(), proposal_id, voter_id, voter_name, vote_type, comment, created_at: now, updated_at: now, } } } /// Simple voting results structure for UI display #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VotingResults { pub proposal_id: String, pub yes_count: usize, pub no_count: usize, pub abstain_count: usize, pub total_votes: usize, } impl VotingResults { pub fn new(proposal_id: String) -> Self { Self { proposal_id, yes_count: 0, no_count: 0, abstain_count: 0, total_votes: 0, } } } /// Controller for handling governance-related routes pub struct GovernanceController; #[allow(dead_code)] impl GovernanceController { /// Helper function to get user from session /// For testing purposes, this will always return a mock user fn get_user_from_session(session: &Session) -> Option { // Try to get user from session first let session_user = session .get::("user") .ok() .flatten() .and_then(|user_json| serde_json::from_str(&user_json).ok()); // If user is not in session, return a mock user for testing session_user.or_else(|| { // Create a mock user let mock_user = serde_json::json!({ "id": 1, "username": "test_user", "email": "test@example.com", "name": "Test User", "role": "member" }); Some(mock_user) }) } /// Calculate statistics from the database fn calculate_statistics_from_database(proposals: &[Proposal]) -> GovernanceStats { let mut stats = GovernanceStats { total_proposals: proposals.len(), active_proposals: 0, approved_proposals: 0, rejected_proposals: 0, draft_proposals: 0, total_votes: 0, participation_rate: 0.0, }; // Count proposals by status for proposal in proposals { match proposal.status { ProposalStatus::Active => stats.active_proposals += 1, ProposalStatus::Approved => stats.approved_proposals += 1, ProposalStatus::Rejected => stats.rejected_proposals += 1, ProposalStatus::Draft => stats.draft_proposals += 1, _ => {} // Handle other statuses if needed } // Count total votes stats.total_votes += proposal.ballots.len(); } // Calculate participation rate (if there are any proposals) if stats.total_proposals > 0 { // This is a simplified calculation - in a real application, you would // calculate this based on the number of eligible voters stats.participation_rate = (stats.total_votes as f64 / stats.total_proposals as f64) * 100.0; } stats } /// Handles the governance dashboard page route pub async fn index(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "dashboard"); // Header data ctx.insert("page_title", "Governance Dashboard"); ctx.insert( "page_description", "Participate in community decision-making", ); ctx.insert("show_create_button", &false); // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); // Get proposals from the database let proposals = match crate::db::governance::get_proposals() { Ok(props) => { // println!( // "📋 Proposals list page: Successfully loaded {} proposals from database", // props.len() // ); for (i, proposal) in props.iter().enumerate() { println!( " Proposal {}: ID={}, title={:?}, status={:?}", i + 1, proposal.base_data.id, proposal.title, proposal.status ); } props } Err(e) => { println!("❌ Proposals list page: Failed to load proposals: {}", e); ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; // Make a copy of proposals for statistics let proposals_for_stats = proposals.clone(); // Filter for active proposals only let active_proposals: Vec = proposals .into_iter() .filter(|p| p.status == heromodels::models::ProposalStatus::Active) .collect(); // Sort active proposals by voting end date (ascending) let mut sorted_active_proposals = active_proposals.clone(); sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date)); ctx.insert("proposals", &sorted_active_proposals); // Get the nearest deadline proposal for the voting pane if let Some(nearest_proposal) = sorted_active_proposals.first() { // Calculate voting results for the nearest proposal let results = Self::calculate_voting_results_from_proposal(nearest_proposal); // Add both the proposal and its results to the context ctx.insert("nearest_proposal", nearest_proposal); ctx.insert("nearest_proposal_results", &results); } // Calculate statistics from the database let stats = Self::calculate_statistics_from_database(&proposals_for_stats); ctx.insert("stats", &stats); // Get recent governance activities from our tracker (limit to 4 for dashboard) let recent_activity = match Self::get_recent_governance_activities() { Ok(activities) => activities.into_iter().take(4).collect::>(), Err(e) => { eprintln!("Failed to load recent activities: {}", e); Vec::new() } }; ctx.insert("recent_activity", &recent_activity); render_template(&tmpl, "governance/index.html", &ctx) } /// Handles the proposal list page route pub async fn proposals( query: web::Query, tmpl: web::Data, session: Session, ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "proposals"); // Header data ctx.insert("page_title", "All Proposals"); ctx.insert( "page_description", "Browse and filter all governance proposals", ); ctx.insert("show_create_button", &false); // Add user to context if available if let Some(user) = Self::get_user_from_session(&session) { ctx.insert("user", &user); } // Get proposals from the database let mut proposals = match get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; // Filter proposals by status if provided if let Some(status_filter) = &query.status { if !status_filter.is_empty() { proposals = proposals .into_iter() .filter(|p| { let proposal_status = format!("{:?}", p.status); proposal_status == *status_filter }) .collect(); } } // Filter by search term if provided (title or description) if let Some(search_term) = &query.search { if !search_term.is_empty() { let search_term = search_term.to_lowercase(); proposals = proposals .into_iter() .filter(|p| { p.title.to_lowercase().contains(&search_term) || p.description.to_lowercase().contains(&search_term) }) .collect(); } } // Add the filtered proposals to the context ctx.insert("proposals", &proposals); // Add the filter values back to the context for form persistence ctx.insert("status_filter", &query.status); ctx.insert("search_filter", &query.search); render_template(&tmpl, "governance/proposals.html", &ctx) } /// Handles the proposal detail page route pub async fn proposal_detail( path: web::Path, req: actix_web::HttpRequest, tmpl: web::Data, session: Session, ) -> Result { // Extract query parameters from the request let query_str = req.query_string(); let vote_success = query_str.contains("vote_success=true"); let proposal_id = path.into_inner(); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "proposals"); // Header data ctx.insert("page_title", "Proposal Details"); ctx.insert( "page_description", "View proposal information and cast your vote", ); ctx.insert("show_create_button", &false); // Add user to context if available if let Some(user) = Self::get_user_from_session(&session) { ctx.insert("user", &user); } // Get mock proposal detail let proposal = get_proposal_by_id(proposal_id.parse().unwrap()); if let Ok(Some(proposal)) = proposal { ctx.insert("proposal", &proposal); // Extract votes directly from the proposal let votes = Self::extract_votes_from_proposal(&proposal); ctx.insert("votes", &votes); // Calculate voting results directly from the proposal let results = Self::calculate_voting_results_from_proposal(&proposal); ctx.insert("results", &results); // Check if vote_success parameter is present and add success message if vote_success { ctx.insert("success", "Your vote has been successfully recorded!"); } render_template(&tmpl, "governance/proposal_detail.html", &ctx) } else { // Proposal not found ctx.insert("error", "Proposal not found"); // For the error page, we'll use a special case to set the status code to 404 match tmpl.render("error.html", &ctx) { Ok(content) => Ok(HttpResponse::NotFound() .content_type("text/html") .body(content)), Err(e) => { eprintln!("Error rendering error template: {}", e); Err(actix_web::error::ErrorInternalServerError(format!( "Error: {}", e ))) } } } } /// Handles the create proposal page route pub async fn create_proposal_form( tmpl: web::Data, session: Session, ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "create"); // Header data ctx.insert("page_title", "Create Proposal"); ctx.insert( "page_description", "Submit a new proposal for community voting", ); ctx.insert("show_create_button", &false); // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); render_template(&tmpl, "governance/create_proposal.html", &ctx) } /// Handles the submission of a new proposal pub async fn submit_proposal( _form: web::Form, tmpl: web::Data, session: Session, ) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); let proposal_title = &_form.title; let proposal_description = &_form.description; // Use the DB-backed proposal creation // Parse voting_start_date and voting_end_date from the form (YYYY-MM-DD expected) let voting_start_date = _form.voting_start_date.as_ref().and_then(|s| { chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") .ok() .and_then(|d| d.and_hms_opt(0, 0, 0)) .map(|naive| chrono::Utc.from_utc_datetime(&naive)) }); let voting_end_date = _form.voting_end_date.as_ref().and_then(|s| { chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") .ok() .and_then(|d| d.and_hms_opt(23, 59, 59)) .map(|naive| chrono::Utc.from_utc_datetime(&naive)) }); // Extract user id and name from serde_json::Value let user_id = user .get("id") .and_then(|v| v.as_i64()) .unwrap_or(1) .to_string(); let user_name = user .get("username") .and_then(|v| v.as_str()) .unwrap_or("Test User") .to_string(); let is_draft = _form.draft.is_some(); let status = if is_draft { ProposalStatus::Draft } else { ProposalStatus::Active }; match governance::create_new_proposal( &user_id, &user_name, proposal_title, proposal_description, status, voting_start_date, voting_end_date, ) { Ok((proposal_id, saved_proposal)) => { println!( "Proposal saved to DB: ID={}, title={:?}", proposal_id, saved_proposal.title ); // Track the proposal creation activity let _ = create_activity( proposal_id, &saved_proposal.title, &user_name, &ActivityType::ProposalCreated, ); ctx.insert("success", "Proposal created successfully!"); } Err(err) => { println!("Failed to save proposal: {err}"); ctx.insert("error", &format!("Failed to save proposal: {err}")); } } // For now, we'll just redirect to the proposals page with a success message // Get proposals from the database let proposals = match crate::db::governance::get_proposals() { Ok(props) => { println!( "✅ Successfully loaded {} proposals from database", props.len() ); for (i, proposal) in props.iter().enumerate() { println!( " Proposal {}: ID={}, title={:?}, status={:?}", i + 1, proposal.base_data.id, proposal.title, proposal.status ); } props } Err(e) => { println!("❌ Failed to load proposals: {}", e); ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; ctx.insert("proposals", &proposals); // Add the required context variables for the proposals template ctx.insert("active_tab", "proposals"); ctx.insert("status_filter", &None::); ctx.insert("search_filter", &None::); // Header data (required by _header.html template) ctx.insert("page_title", "All Proposals"); ctx.insert( "page_description", "Browse and filter all governance proposals", ); ctx.insert("show_create_button", &false); render_template(&tmpl, "governance/proposals.html", &ctx) } /// Handles the submission of a vote on a proposal pub async fn submit_vote( path: web::Path, form: web::Form, tmpl: web::Data, session: Session, ) -> Result { let proposal_id = path.into_inner(); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); // Check if user is logged in let user = match Self::get_user_from_session(&session) { Some(user) => user, None => { return Ok(HttpResponse::Found() .append_header(("Location", "/login")) .finish()); } }; ctx.insert("user", &user); // Extract user ID let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; // Parse proposal ID let proposal_id_u32 = match proposal_id.parse::() { Ok(id) => id, Err(_) => { ctx.insert("error", "Invalid proposal ID"); return render_template(&tmpl, "error.html", &ctx); } }; // Submit the vote match crate::db::governance::submit_vote_on_proposal( proposal_id_u32, user_id, &form.vote_type, 1, // Default to 1 share form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form ) { Ok(_) => { // Record the vote activity let user_name = user .get("username") .and_then(|v| v.as_str()) .unwrap_or("Unknown User"); // Track the vote cast activity if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) { let _ = create_activity( proposal_id_u32, &proposal.title, user_name, &ActivityType::VoteCast, ); } // Redirect to the proposal detail page with a success message return Ok(HttpResponse::Found() .append_header(( "Location", format!("/governance/proposals/{}?vote_success=true", proposal_id), )) .finish()); } Err(e) => { ctx.insert("error", &format!("Failed to submit vote: {}", e)); render_template(&tmpl, "error.html", &ctx) } } } /// Handles the my votes page route pub async fn my_votes(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "my_votes"); // Header data ctx.insert("page_title", "My Votes"); ctx.insert( "page_description", "View your voting history and participation", ); ctx.insert("show_create_button", &false); // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); // Extract user ID let user_id = user.get("id").and_then(|v| v.as_i64()).unwrap_or(1) as i32; // Get all proposals from the database let proposals = match crate::db::governance::get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; // Extract votes for this user from all proposals let mut user_votes = Vec::new(); for proposal in &proposals { // Extract votes from this proposal let votes = Self::extract_votes_from_proposal(proposal); // Filter votes for this user for vote in votes { if vote.voter_id == user_id { user_votes.push((vote, proposal.clone())); } } } // Calculate total vote counts for all proposals let total_vote_counts = Self::calculate_total_vote_counts(&proposals); ctx.insert("total_yes_votes", &total_vote_counts.0); ctx.insert("total_no_votes", &total_vote_counts.1); ctx.insert("total_abstain_votes", &total_vote_counts.2); ctx.insert("votes", &user_votes); render_template(&tmpl, "governance/my_votes.html", &ctx) } /// Handles the all activities page route pub async fn all_activities(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "activities"); // Header data ctx.insert("page_title", "All Governance Activities"); ctx.insert( "page_description", "Complete history of governance actions and events", ); ctx.insert("show_create_button", &false); // Add user to context (will always be available with our mock user) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); // Get all governance activities from the database let activities = match Self::get_all_governance_activities() { Ok(activities) => activities, Err(e) => { eprintln!("Failed to load all activities: {}", e); Vec::new() } }; ctx.insert("activities", &activities); render_template(&tmpl, "governance/all_activities.html", &ctx) } /// Get recent governance activities from the database fn get_recent_governance_activities() -> Result, String> { // Get real activities from the database (no demo data) let activities = get_recent_activities()?; // Convert GovernanceActivity to the format expected by the template let formatted_activities: Vec = activities .into_iter() .map(|activity| { // Map activity type to appropriate icon let (icon, action) = match activity.activity_type.as_str() { "proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"), "vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"), "voting_started" => ("bi-play-circle-fill text-info", "started voting"), "voting_ended" => ("bi-clock-fill text-warning", "ended voting"), "proposal_status_changed" => ("bi-shield-check text-success", "changed status"), "vote_option_added" => ("bi-list-ul text-secondary", "added vote option"), _ => ("bi-circle-fill text-muted", "performed action"), }; serde_json::json!({ "type": activity.activity_type, "icon": icon, "user": activity.creator_name, "action": action, "proposal_title": activity.proposal_title, "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "proposal_id": activity.proposal_id }) }) .collect(); Ok(formatted_activities) } /// Get all governance activities from the database fn get_all_governance_activities() -> Result, String> { // Get all activities from the database let activities = get_all_activities()?; // Convert GovernanceActivity to the format expected by the template let formatted_activities: Vec = activities .into_iter() .map(|activity| { // Map activity type to appropriate icon let (icon, action) = match activity.activity_type.as_str() { "proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"), "vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"), "voting_started" => ("bi-play-circle-fill text-info", "started voting"), "voting_ended" => ("bi-clock-fill text-warning", "ended voting"), "proposal_status_changed" => ("bi-shield-check text-success", "changed status"), "vote_option_added" => ("bi-list-ul text-secondary", "added vote option"), _ => ("bi-circle-fill text-muted", "performed action"), }; serde_json::json!({ "type": activity.activity_type, "icon": icon, "user": activity.creator_name, "action": action, "proposal_title": activity.proposal_title, "created_at": activity.created_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(), "proposal_id": activity.proposal_id }) }) .collect(); Ok(formatted_activities) } /// Generate mock votes for a specific proposal fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec { let now = Utc::now(); vec![ Vote { id: "vote-001".to_string(), proposal_id: proposal_id.to_string(), voter_id: 1, voter_name: "Robert Callingham".to_string(), vote_type: VoteType::Yes, comment: Some("I strongly support this initiative.".to_string()), created_at: now - Duration::days(2), updated_at: now - Duration::days(2), }, Vote { id: "vote-002".to_string(), proposal_id: proposal_id.to_string(), voter_id: 2, voter_name: "Jane Smith".to_string(), vote_type: VoteType::Yes, comment: None, created_at: now - Duration::days(2), updated_at: now - Duration::days(2), }, Vote { id: "vote-003".to_string(), proposal_id: proposal_id.to_string(), voter_id: 3, voter_name: "Bob Johnson".to_string(), vote_type: VoteType::No, comment: Some("I have concerns about the implementation cost.".to_string()), created_at: now - Duration::days(1), updated_at: now - Duration::days(1), }, Vote { id: "vote-004".to_string(), proposal_id: proposal_id.to_string(), voter_id: 4, voter_name: "Alice Williams".to_string(), vote_type: VoteType::Abstain, comment: Some("I need more information before making a decision.".to_string()), created_at: now - Duration::hours(12), updated_at: now - Duration::hours(12), }, ] } /// Calculate voting results from a proposal fn calculate_voting_results_from_proposal(proposal: &Proposal) -> VotingResults { let mut results = VotingResults::new(proposal.base_data.id.to_string()); // Count votes for each option for option in &proposal.options { match option.id { 1 => results.yes_count = option.count as usize, 2 => results.no_count = option.count as usize, 3 => results.abstain_count = option.count as usize, _ => {} // Ignore other options } } // Calculate total votes results.total_votes = results.yes_count + results.no_count + results.abstain_count; results } /// Extract votes from a proposal's ballots fn extract_votes_from_proposal(proposal: &Proposal) -> Vec { let mut votes = Vec::new(); // Debug: Print proposal ID and number of ballots println!( "Extracting votes from proposal ID: {}", proposal.base_data.id ); println!("Number of ballots in proposal: {}", proposal.ballots.len()); // If there are no ballots, create some mock votes for testing if proposal.ballots.is_empty() { println!("No ballots found in proposal, creating mock votes for testing"); // Create mock votes based on the option counts for option in &proposal.options { if option.count > 0 { let vote_type = match option.id { 1 => VoteType::Yes, 2 => VoteType::No, 3 => VoteType::Abstain, _ => continue, }; // Create a mock vote for each count for i in 0..option.count { let vote = Vote::new( proposal.base_data.id.to_string(), i as i32 + 1, format!("User {}", i + 1), vote_type.clone(), option.comment.clone(), ); votes.push(vote); } } } println!("Created {} mock votes", votes.len()); return votes; } // Convert each ballot to a Vote for (i, ballot) in proposal.ballots.iter().enumerate() { println!( "Processing ballot {}: user_id={}, option_id={}, shares={}", i, ballot.user_id, ballot.vote_option_id, ballot.shares_count ); // Map option_id to VoteType let vote_type = match ballot.vote_option_id { 1 => VoteType::Yes, 2 => VoteType::No, 3 => VoteType::Abstain, _ => { println!( "Unknown option_id: {}, defaulting to Abstain", ballot.vote_option_id ); VoteType::Abstain // Default to Abstain for unknown options } }; // Convert user_id from u32 to i32 safely let voter_id = match i32::try_from(ballot.user_id) { Ok(id) => id, Err(e) => { println!("Failed to convert user_id {} to i32: {}", ballot.user_id, e); continue; // Skip this ballot if conversion fails } }; let ballot_timestamp = match chrono::DateTime::from_timestamp(ballot.base_data.created_at, 0) { 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_name: format!("User {}", voter_id), vote_type, 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 } // The calculate_statistics_from_database function is now defined at the top of the impl block /// Calculate total vote counts across all proposals /// Returns a tuple of (yes_count, no_count, abstain_count) fn calculate_total_vote_counts(proposals: &[Proposal]) -> (usize, usize, usize) { let mut yes_count = 0; let mut no_count = 0; let mut abstain_count = 0; for proposal in proposals { // Extract votes from this proposal let votes = Self::extract_votes_from_proposal(proposal); // Count votes by type for vote in votes { match vote.vote_type { VoteType::Yes => yes_count += 1, VoteType::No => no_count += 1, VoteType::Abstain => abstain_count += 1, } } } (yes_count, no_count, abstain_count) } } /// Represents the data submitted in the proposal form #[derive(Debug, Deserialize)] pub struct ProposalForm { /// Title of the proposal pub title: String, /// Description of the proposal pub description: String, /// Status of the proposal pub draft: Option, /// Start date for voting pub voting_start_date: Option, /// End date for voting pub voting_end_date: Option, } /// Represents the data submitted in the vote form #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct VoteForm { /// Type of vote (yes, no, abstain) pub vote_type: String, /// Optional comment explaining the vote pub comment: Option, } /// Query parameters for filtering proposals #[derive(Debug, Deserialize)] pub struct ProposalQuery { pub status: Option, pub search: Option, } /// Represents statistics for the governance dashboard #[derive(Debug, Serialize)] pub struct GovernanceStats { /// Total number of proposals pub total_proposals: usize, /// Number of active proposals pub active_proposals: usize, /// Number of approved proposals pub approved_proposals: usize, /// Number of rejected proposals pub rejected_proposals: usize, /// Number of draft proposals pub draft_proposals: usize, /// Total number of votes cast pub total_votes: usize, /// Participation rate (percentage) pub participation_rate: f64, }