use crate::db::proposals::{self, get_proposal_by_id}; use crate::models::governance::{Vote, VoteType, VotingResults}; use crate::utils::render_template; use actix_session::Session; use actix_web::{HttpResponse, Responder, Result, web}; use chrono::{Duration, Utc}; use heromodels::models::governance::{Proposal, ProposalStatus}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tera::Tera; use chrono::prelude::*; /// 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) }) } /// 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"); // 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::proposals::get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; // 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() { ctx.insert("nearest_proposal", nearest_proposal); } // Get recent activity for the timeline let recent_activity = Self::get_mock_recent_activity(); ctx.insert("recent_activity", &recent_activity); // Get some statistics let stats = Self::get_mock_statistics(); ctx.insert("stats", &stats); render_template(&tmpl, "governance/index.html", &ctx) } /// Handles the proposal list page route pub async fn proposals(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); ctx.insert("active_tab", "proposals"); // 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 proposals = match crate::db::proposals::get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; ctx.insert("proposals", &proposals); render_template(&tmpl, "governance/proposals.html", &ctx) } /// Handles the proposal detail page route pub async fn proposal_detail( path: web::Path, tmpl: web::Data, session: Session, ) -> Result { let proposal_id = path.into_inner(); let mut ctx = tera::Context::new(); ctx.insert("active_page", "governance"); // 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); // Get mock votes for this proposal let votes = Self::get_mock_votes_for_proposal(&proposal_id); ctx.insert("votes", &votes); // Calculate voting results directly from the proposal let results = Self::calculate_voting_results_from_proposal(&proposal); ctx.insert("results", &results); 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"); // 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 proposals::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 ); 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::proposals::get_proposals() { Ok(props) => props, Err(e) => { ctx.insert("error", &format!("Failed to load proposals: {}", e)); vec![] } }; ctx.insert("proposals", &proposals); 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::proposals::submit_vote_on_proposal( proposal_id_u32, user_id, &form.vote_type, 1, // Default to 1 share ) { Ok(updated_proposal) => { ctx.insert("proposal", &updated_proposal); ctx.insert("success", "Your vote has been recorded!"); // Get votes for this proposal // For now, we'll still use mock votes until we implement a function to extract votes from the proposal let votes = Self::get_mock_votes_for_proposal(&proposal_id); ctx.insert("votes", &votes); // Calculate voting results directly from the updated proposal let results = Self::calculate_voting_results_from_proposal(&updated_proposal); ctx.insert("results", &results); render_template(&tmpl, "governance/proposal_detail.html", &ctx) } 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"); // 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 mock votes for this user let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data ctx.insert("votes", &votes); render_template(&tmpl, "governance/my_votes.html", &ctx) } /// Generate mock recent activity data for the dashboard fn get_mock_recent_activity() -> Vec { vec![ serde_json::json!({ "type": "vote", "user": "Sarah Johnson", "proposal_id": "prop-001", "proposal_title": "Community Garden Initiative", "action": "voted Yes", "timestamp": (Utc::now() - Duration::hours(2)).to_rfc3339(), "icon": "bi-check-circle-fill text-success" }), serde_json::json!({ "type": "comment", "user": "Michael Chen", "proposal_id": "prop-003", "proposal_title": "Weekly Community Calls", "action": "commented", "comment": "I think this would greatly improve communication.", "timestamp": (Utc::now() - Duration::hours(5)).to_rfc3339(), "icon": "bi-chat-left-text-fill text-primary" }), serde_json::json!({ "type": "vote", "user": "Robert Callingham", "proposal_id": "prop-005", "proposal_title": "Security Audit Implementation", "action": "voted Yes", "timestamp": (Utc::now() - Duration::hours(8)).to_rfc3339(), "icon": "bi-check-circle-fill text-success" }), serde_json::json!({ "type": "proposal", "user": "Emma Rodriguez", "proposal_id": "prop-004", "proposal_title": "Sustainability Roadmap", "action": "created proposal", "timestamp": (Utc::now() - Duration::hours(12)).to_rfc3339(), "icon": "bi-file-earmark-text-fill text-info" }), serde_json::json!({ "type": "vote", "user": "David Kim", "proposal_id": "prop-002", "proposal_title": "Governance Framework Update", "action": "voted No", "timestamp": (Utc::now() - Duration::hours(16)).to_rfc3339(), "icon": "bi-x-circle-fill text-danger" }), serde_json::json!({ "type": "comment", "user": "Lisa Wang", "proposal_id": "prop-001", "proposal_title": "Community Garden Initiative", "action": "commented", "comment": "I'd like to volunteer to help coordinate this effort.", "timestamp": (Utc::now() - Duration::hours(24)).to_rfc3339(), "icon": "bi-chat-left-text-fill text-primary" }), ] } // Mock data generation methods /// Generate mock proposals for testing fn get_mock_proposals() -> Vec { let now = Utc::now(); vec![ Proposal::new( Some(1), "1", "Ibrahim Faraji", "Establish Zanzibar Digital Trade Hub", "This proposal aims to create a dedicated digital trade hub within the Zanzibar Digital Freezone to facilitate international e-commerce for local businesses. The hub will provide logistics support, digital marketing services, and regulatory compliance assistance to help Zanzibar businesses reach global markets.", ProposalStatus::Active, now - Duration::days(5), now - Duration::days(5), now - Duration::days(3), now + Duration::days(4), ), Proposal::new( Some(2), "2", "Amina Salim", "ZDFZ Sustainable Tourism Framework", "A comprehensive framework for sustainable tourism development within the Zanzibar Digital Freezone. This proposal outlines environmental standards, community benefit-sharing mechanisms, and digital infrastructure for eco-tourism businesses. It includes tokenization standards for tourism assets and a certification system for sustainable operators.", ProposalStatus::Approved, now - Duration::days(15), now - Duration::days(2), now - Duration::days(14), now - Duration::days(2), ), Proposal::new( Some(3), "3", "Hassan Mwinyi", "Spice Industry Modernization Initiative", "This proposal seeks to modernize Zanzibar's traditional spice industry through blockchain-based supply chain tracking, international quality certification, and digital marketplace integration. The initiative will help local spice farmers and processors access premium international markets while preserving traditional cultivation methods.", ProposalStatus::Draft, now - Duration::days(1), now - Duration::days(1), now - Duration::days(1), now + Duration::days(1), ), Proposal::new( Some(4), "4", "Ibrahim Faraji", "ZDFZ Regulatory Framework for Digital Financial Services", "Establish a comprehensive regulatory framework for digital financial services within the Zanzibar Digital Freezone. This includes licensing requirements for crypto exchanges, digital payment providers, and tokenized asset platforms operating within the zone, while ensuring compliance with international AML/KYC standards.", ProposalStatus::Rejected, now - Duration::days(20), now - Duration::days(5), now - Duration::days(19), now - Duration::days(5), ), Proposal::new( Some(5), "5", "Fatma Busaidy", "Digital Arts Incubator and Artwork Marketplace", "Create a dedicated digital arts incubator and Artwork marketplace to support Zanzibar's creative economy. The initiative will provide technical training, equipment, and a curated marketplace for local artists to create and sell digital art that celebrates Zanzibar's rich cultural heritage while accessing global markets.", ProposalStatus::Active, now - Duration::days(7), now - Duration::days(7), now - Duration::days(6), now + Duration::days(1), ), Proposal::new( Some(6), "6", "Omar Makame", "Zanzibar Renewable Energy Microgrid Network", "Develop a network of renewable energy microgrids across the Zanzibar Digital Freezone using tokenized investment and community ownership models. This proposal outlines the technical specifications, governance structure, and token economics for deploying solar and tidal energy systems that will ensure energy independence for the zone.", ProposalStatus::Active, now - Duration::days(10), now - Duration::days(9), now - Duration::days(8), now + Duration::days(6), ), Proposal::new( Some(7), "7", "Saida Juma", "ZDFZ Educational Technology Initiative", "Establish a comprehensive educational technology program within the Zanzibar Digital Freezone to develop local tech talent. This initiative includes coding academies, blockchain development courses, and digital entrepreneurship training, with a focus on preparing Zanzibar's youth for careers in the zone's growing digital economy.", ProposalStatus::Draft, now - Duration::days(3), now - Duration::days(2), now - Duration::days(1), now + Duration::days(1), ), ] } /// Get a mock proposal by ID fn get_mock_proposal_by_id(id: &str) -> Option { Self::get_mock_proposals() .into_iter() .find(|p| p.base_data.id.to_string() == id) } /// 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), }, ] } /// Generate mock votes for a specific user fn get_mock_votes_for_user(user_id: i32) -> Vec<(Vote, Proposal)> { let votes = vec![ Vote { id: "vote-001".to_string(), proposal_id: "prop-001".to_string(), voter_id: user_id, voter_name: "Robert Callingham".to_string(), vote_type: VoteType::Yes, comment: Some("I strongly support this initiative.".to_string()), created_at: Utc::now() - Duration::days(2), updated_at: Utc::now() - Duration::days(2), }, Vote { id: "vote-005".to_string(), proposal_id: "prop-002".to_string(), voter_id: user_id, voter_name: "Robert Callingham".to_string(), vote_type: VoteType::No, comment: Some("I don't think this is a priority right now.".to_string()), created_at: Utc::now() - Duration::days(10), updated_at: Utc::now() - Duration::days(10), }, Vote { id: "vote-008".to_string(), proposal_id: "prop-004".to_string(), voter_id: user_id, voter_name: "Robert Callingham".to_string(), vote_type: VoteType::Yes, comment: None, created_at: Utc::now() - Duration::days(18), updated_at: Utc::now() - Duration::days(18), }, Vote { id: "vote-010".to_string(), proposal_id: "prop-005".to_string(), voter_id: user_id, voter_name: "Robert Callingham".to_string(), vote_type: VoteType::Yes, comment: Some("Security is always a top priority.".to_string()), created_at: Utc::now() - Duration::days(5), updated_at: Utc::now() - Duration::days(5), }, ]; let proposals = Self::get_mock_proposals(); votes .into_iter() .filter_map(|vote| { proposals .iter() .find(|p| p.base_data.id.to_string() == vote.proposal_id) .map(|p| (vote.clone(), p.clone())) }) .collect() } /// Generate mock voting results for a proposal fn get_mock_voting_results(proposal_id: &str) -> VotingResults { let votes = Self::get_mock_votes_for_proposal(proposal_id); let mut results = VotingResults::new(proposal_id.to_string()); for vote in votes { results.add_vote(&vote.vote_type); } results } /// 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 } /// Generate mock statistics for the governance dashboard fn get_mock_statistics() -> GovernanceStats { GovernanceStats { total_proposals: 5, active_proposals: 2, approved_proposals: 1, rejected_proposals: 1, draft_proposals: 1, total_votes: 15, participation_rate: 75.0, } } } /// 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, } /// 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, }