488 lines
21 KiB
Rust
488 lines
21 KiB
Rust
use actix_web::{web, HttpResponse, Responder, Result};
|
|
use actix_session::Session;
|
|
use tera::Tera;
|
|
use serde_json::Value;
|
|
use serde::{Deserialize, Serialize};
|
|
use chrono::{Utc, Duration};
|
|
use crate::models::governance::{Proposal, Vote, ProposalStatus, VoteType, VotingResults};
|
|
use crate::utils::render_template;
|
|
|
|
/// Controller for handling governance-related routes
|
|
pub struct GovernanceController;
|
|
|
|
impl GovernanceController {
|
|
/// Helper function to get user from session
|
|
fn get_user_from_session(session: &Session) -> Option<Value> {
|
|
session.get::<String>("user").ok().flatten().and_then(|user_json| {
|
|
serde_json::from_str(&user_json).ok()
|
|
})
|
|
}
|
|
|
|
/// 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");
|
|
|
|
// Add user to context if available
|
|
if let Some(user) = Self::get_user_from_session(&session) {
|
|
ctx.insert("user", &user);
|
|
}
|
|
|
|
// Get mock proposals for the dashboard
|
|
let proposals = Self::get_mock_proposals();
|
|
ctx.insert("proposals", &proposals);
|
|
|
|
// 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<Tera>, session: Session) -> Result<impl Responder> {
|
|
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 mock proposals
|
|
let proposals = Self::get_mock_proposals();
|
|
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<String>,
|
|
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");
|
|
|
|
// 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 = Self::get_mock_proposal_by_id(&proposal_id);
|
|
if let 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);
|
|
|
|
// Get voting results
|
|
let results = Self::get_mock_voting_results(&proposal_id);
|
|
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<Tera>, session: Session) -> Result<impl Responder> {
|
|
let mut ctx = tera::Context::new();
|
|
ctx.insert("active_page", "governance");
|
|
ctx.insert("active_tab", "create");
|
|
|
|
// Add user to context if available
|
|
if let Some(user) = Self::get_user_from_session(&session) {
|
|
ctx.insert("user", &user);
|
|
} else {
|
|
// Redirect to login if not logged in
|
|
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
|
|
}
|
|
|
|
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> {
|
|
// Check if user is logged in
|
|
if Self::get_user_from_session(&session).is_none() {
|
|
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// In a real application, we would save the proposal to a database
|
|
// For now, we'll just redirect to the proposals page with a success message
|
|
ctx.insert("success", "Proposal created successfully!");
|
|
|
|
// Get mock proposals
|
|
let proposals = Self::get_mock_proposals();
|
|
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<String>,
|
|
_form: web::Form<VoteForm>,
|
|
tmpl: web::Data<Tera>,
|
|
session: Session
|
|
) -> Result<impl Responder> {
|
|
let proposal_id = path.into_inner();
|
|
|
|
// Check if user is logged in
|
|
if Self::get_user_from_session(&session).is_none() {
|
|
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
|
|
}
|
|
|
|
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 = Self::get_mock_proposal_by_id(&proposal_id);
|
|
if let Some(proposal) = proposal {
|
|
ctx.insert("proposal", &proposal);
|
|
ctx.insert("success", "Your vote has been recorded!");
|
|
|
|
// Get mock votes for this proposal
|
|
let votes = Self::get_mock_votes_for_proposal(&proposal_id);
|
|
ctx.insert("votes", &votes);
|
|
|
|
// Get voting results
|
|
let results = Self::get_mock_voting_results(&proposal_id);
|
|
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 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");
|
|
|
|
// Add user to context if available
|
|
if let Some(user) = Self::get_user_from_session(&session) {
|
|
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)
|
|
} else {
|
|
// Redirect to login if not logged in
|
|
Ok(HttpResponse::Found().append_header(("Location", "/login")).finish())
|
|
}
|
|
}
|
|
|
|
// Mock data generation methods
|
|
|
|
/// Generate mock proposals for testing
|
|
fn get_mock_proposals() -> Vec<Proposal> {
|
|
let now = Utc::now();
|
|
vec![
|
|
Proposal {
|
|
id: "prop-001".to_string(),
|
|
creator_id: 1,
|
|
creator_name: "Ibrahim Faraji".to_string(),
|
|
title: "Establish Zanzibar Digital Trade Hub".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Active,
|
|
created_at: now - Duration::days(5),
|
|
updated_at: now - Duration::days(5),
|
|
voting_starts_at: Some(now - Duration::days(3)),
|
|
voting_ends_at: Some(now + Duration::days(4)),
|
|
},
|
|
Proposal {
|
|
id: "prop-002".to_string(),
|
|
creator_id: 2,
|
|
creator_name: "Amina Salim".to_string(),
|
|
title: "ZDFZ Sustainable Tourism Framework".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Approved,
|
|
created_at: now - Duration::days(15),
|
|
updated_at: now - Duration::days(2),
|
|
voting_starts_at: Some(now - Duration::days(14)),
|
|
voting_ends_at: Some(now - Duration::days(2)),
|
|
},
|
|
Proposal {
|
|
id: "prop-003".to_string(),
|
|
creator_id: 3,
|
|
creator_name: "Hassan Mwinyi".to_string(),
|
|
title: "Spice Industry Modernization Initiative".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Draft,
|
|
created_at: now - Duration::days(1),
|
|
updated_at: now - Duration::days(1),
|
|
voting_starts_at: None,
|
|
voting_ends_at: None,
|
|
},
|
|
Proposal {
|
|
id: "prop-004".to_string(),
|
|
creator_id: 1,
|
|
creator_name: "Ibrahim Faraji".to_string(),
|
|
title: "ZDFZ Regulatory Framework for Digital Financial Services".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Rejected,
|
|
created_at: now - Duration::days(20),
|
|
updated_at: now - Duration::days(5),
|
|
voting_starts_at: Some(now - Duration::days(19)),
|
|
voting_ends_at: Some(now - Duration::days(5)),
|
|
},
|
|
Proposal {
|
|
id: "prop-005".to_string(),
|
|
creator_id: 4,
|
|
creator_name: "Fatma Busaidy".to_string(),
|
|
title: "Digital Arts Incubator and Artwork Marketplace".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Active,
|
|
created_at: now - Duration::days(7),
|
|
updated_at: now - Duration::days(7),
|
|
voting_starts_at: Some(now - Duration::days(6)),
|
|
voting_ends_at: Some(now + Duration::days(1)),
|
|
},
|
|
Proposal {
|
|
id: "prop-006".to_string(),
|
|
creator_id: 5,
|
|
creator_name: "Omar Makame".to_string(),
|
|
title: "Zanzibar Renewable Energy Microgrid Network".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Active,
|
|
created_at: now - Duration::days(10),
|
|
updated_at: now - Duration::days(9),
|
|
voting_starts_at: Some(now - Duration::days(8)),
|
|
voting_ends_at: Some(now + Duration::days(6)),
|
|
},
|
|
Proposal {
|
|
id: "prop-007".to_string(),
|
|
creator_id: 6,
|
|
creator_name: "Saida Juma".to_string(),
|
|
title: "ZDFZ Educational Technology Initiative".to_string(),
|
|
description: "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.".to_string(),
|
|
status: ProposalStatus::Draft,
|
|
created_at: now - Duration::days(3),
|
|
updated_at: now - Duration::days(2),
|
|
voting_starts_at: None,
|
|
voting_ends_at: None,
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Get a mock proposal by ID
|
|
fn get_mock_proposal_by_id(id: &str) -> Option<Proposal> {
|
|
Self::get_mock_proposals().into_iter().find(|p| p.id == id)
|
|
}
|
|
|
|
/// 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),
|
|
},
|
|
]
|
|
}
|
|
|
|
/// 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.id == 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
|
|
}
|
|
|
|
/// 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,
|
|
/// 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)]
|
|
pub struct VoteForm {
|
|
/// Type of vote (yes, no, abstain)
|
|
pub vote_type: String,
|
|
/// Optional comment explaining the vote
|
|
pub comment: 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,
|
|
}
|