hostbasket/actix_mvc_app/src/controllers/governance.rs
Mahmoud-Emad 7e95391a9c feat: Refactor governance models and views
- Moved governance models (`Vote`, `VoteType`, `VotingResults`) from
  `models/governance.rs` to `controllers/governance.rs` for better
  organization and to avoid circular dependencies.  This improves
  maintainability and reduces complexity.
- Updated governance views to use the new model locations.
- Added a limit to the number of recent activities displayed on the
  dashboard for performance optimization.
2025-06-03 15:31:50 +03:00

993 lines
36 KiB
Rust

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<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Vote {
pub fn new(
proposal_id: String,
voter_id: i32,
voter_name: String,
vote_type: VoteType,
comment: Option<String>,
) -> 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<Value> {
// Try to get user from session first
let session_user = session
.get::<String>("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<Tera>, session: Session) -> Result<impl Responder> {
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<heromodels::models::Proposal> = 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::<Vec<_>>(),
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<ProposalQuery>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
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<String>,
req: actix_web::HttpRequest,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
// 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<Tera>,
session: Session,
) -> Result<impl Responder> {
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<ProposalForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
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::<String>);
ctx.insert("search_filter", &None::<String>);
// 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<String>,
form: web::Form<VoteForm>,
tmpl: web::Data<Tera>,
session: Session,
) -> Result<impl Responder> {
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::<u32>() {
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<Tera>, session: Session) -> Result<impl Responder> {
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<Tera>, session: Session) -> Result<impl Responder> {
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<Vec<Value>, 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<Value> = 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<Vec<Value>, 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<Value> = 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<Vote> {
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<Vote> {
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<bool>,
/// Start date for voting
pub voting_start_date: Option<String>,
/// End date for voting
pub voting_end_date: Option<String>,
}
/// 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<String>,
}
/// Query parameters for filtering proposals
#[derive(Debug, Deserialize)]
pub struct ProposalQuery {
pub status: Option<String>,
pub search: Option<String>,
}
/// 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,
}