feat: Implement Proposals page:
- Added the create new proposal functionality - Added the list all proposals functionnality
This commit is contained in:
parent
60198dc2d4
commit
4a2f1c7282
@ -1,9 +1,10 @@
|
|||||||
use crate::db::proposals;
|
use crate::db::proposals;
|
||||||
use crate::models::governance::{Proposal, ProposalStatus, Vote, VoteType, VotingResults};
|
use crate::models::governance::{Vote, VoteType, VotingResults};
|
||||||
use crate::utils::render_template;
|
use crate::utils::render_template;
|
||||||
use actix_session::Session;
|
use actix_session::Session;
|
||||||
use actix_web::{HttpResponse, Responder, Result, web};
|
use actix_web::{HttpResponse, Responder, Result, web};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
use heromodels::models::governance::{Proposal, ProposalStatus};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
@ -48,18 +49,24 @@ impl GovernanceController {
|
|||||||
let user = Self::get_user_from_session(&session).unwrap();
|
let user = Self::get_user_from_session(&session).unwrap();
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
|
|
||||||
// Get mock proposals for the dashboard
|
// Get proposals from the database
|
||||||
let proposals = Self::get_mock_proposals();
|
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
|
// Filter for active proposals only
|
||||||
let active_proposals: Vec<Proposal> = proposals
|
let active_proposals: Vec<heromodels::models::Proposal> = proposals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| p.status == ProposalStatus::Active)
|
.filter(|p| p.status == heromodels::models::ProposalStatus::Active)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Sort active proposals by voting end date (ascending)
|
// Sort active proposals by voting end date (ascending)
|
||||||
let mut sorted_active_proposals = active_proposals.clone();
|
let mut sorted_active_proposals = active_proposals.clone();
|
||||||
sorted_active_proposals.sort_by(|a, b| a.voting_ends_at.cmp(&b.voting_ends_at));
|
sorted_active_proposals.sort_by(|a, b| a.vote_start_date.cmp(&b.vote_end_date));
|
||||||
|
|
||||||
ctx.insert("proposals", &sorted_active_proposals);
|
ctx.insert("proposals", &sorted_active_proposals);
|
||||||
|
|
||||||
@ -90,8 +97,14 @@ impl GovernanceController {
|
|||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mock proposals
|
// Get proposals from the database
|
||||||
let proposals = Self::get_mock_proposals();
|
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);
|
ctx.insert("proposals", &proposals);
|
||||||
|
|
||||||
render_template(&tmpl, "governance/proposals.html", &ctx)
|
render_template(&tmpl, "governance/proposals.html", &ctx)
|
||||||
@ -199,10 +212,24 @@ impl GovernanceController {
|
|||||||
.unwrap_or(1)
|
.unwrap_or(1)
|
||||||
.to_string();
|
.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(
|
match proposals::create_new_proposal(
|
||||||
&user_id,
|
&user_id,
|
||||||
|
&user_name,
|
||||||
proposal_title,
|
proposal_title,
|
||||||
proposal_description,
|
proposal_description,
|
||||||
|
status,
|
||||||
voting_start_date,
|
voting_start_date,
|
||||||
voting_end_date,
|
voting_end_date,
|
||||||
) {
|
) {
|
||||||
@ -221,8 +248,14 @@ impl GovernanceController {
|
|||||||
|
|
||||||
// For now, we'll just redirect to the proposals page with a success message
|
// For now, we'll just redirect to the proposals page with a success message
|
||||||
|
|
||||||
// Get mock proposals
|
// Get proposals from the database
|
||||||
let proposals = Self::get_mock_proposals();
|
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);
|
ctx.insert("proposals", &proposals);
|
||||||
|
|
||||||
render_template(&tmpl, "governance/proposals.html", &ctx)
|
render_template(&tmpl, "governance/proposals.html", &ctx)
|
||||||
@ -371,96 +404,98 @@ impl GovernanceController {
|
|||||||
fn get_mock_proposals() -> Vec<Proposal> {
|
fn get_mock_proposals() -> Vec<Proposal> {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
vec![
|
vec![
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-001".to_string(),
|
Some(1),
|
||||||
creator_id: 1,
|
"1",
|
||||||
creator_name: "Ibrahim Faraji".to_string(),
|
"Ibrahim Faraji",
|
||||||
title: "Establish Zanzibar Digital Trade Hub".to_string(),
|
"Establish Zanzibar Digital Trade Hub",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Active,
|
ProposalStatus::Active,
|
||||||
created_at: now - Duration::days(5),
|
now - Duration::days(5),
|
||||||
updated_at: now - Duration::days(5),
|
now - Duration::days(5),
|
||||||
voting_starts_at: Some(now - Duration::days(3)),
|
now - Duration::days(3),
|
||||||
voting_ends_at: Some(now + Duration::days(4)),
|
now + Duration::days(4),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-002".to_string(),
|
Some(2),
|
||||||
creator_id: 2,
|
"2",
|
||||||
creator_name: "Amina Salim".to_string(),
|
"Amina Salim",
|
||||||
title: "ZDFZ Sustainable Tourism Framework".to_string(),
|
"ZDFZ Sustainable Tourism Framework",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Approved,
|
ProposalStatus::Approved,
|
||||||
created_at: now - Duration::days(15),
|
now - Duration::days(15),
|
||||||
updated_at: now - Duration::days(2),
|
now - Duration::days(2),
|
||||||
voting_starts_at: Some(now - Duration::days(14)),
|
now - Duration::days(14),
|
||||||
voting_ends_at: Some(now - Duration::days(2)),
|
now - Duration::days(2),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-003".to_string(),
|
Some(3),
|
||||||
creator_id: 3,
|
"3",
|
||||||
creator_name: "Hassan Mwinyi".to_string(),
|
"Hassan Mwinyi",
|
||||||
title: "Spice Industry Modernization Initiative".to_string(),
|
"Spice Industry Modernization Initiative",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Draft,
|
ProposalStatus::Draft,
|
||||||
created_at: now - Duration::days(1),
|
now - Duration::days(1),
|
||||||
updated_at: now - Duration::days(1),
|
now - Duration::days(1),
|
||||||
voting_starts_at: None,
|
now - Duration::days(1),
|
||||||
voting_ends_at: None,
|
now + Duration::days(1),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-004".to_string(),
|
Some(4),
|
||||||
creator_id: 1,
|
"4",
|
||||||
creator_name: "Ibrahim Faraji".to_string(),
|
"Ibrahim Faraji",
|
||||||
title: "ZDFZ Regulatory Framework for Digital Financial Services".to_string(),
|
"ZDFZ Regulatory Framework for Digital Financial Services",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Rejected,
|
ProposalStatus::Rejected,
|
||||||
created_at: now - Duration::days(20),
|
now - Duration::days(20),
|
||||||
updated_at: now - Duration::days(5),
|
now - Duration::days(5),
|
||||||
voting_starts_at: Some(now - Duration::days(19)),
|
now - Duration::days(19),
|
||||||
voting_ends_at: Some(now - Duration::days(5)),
|
now - Duration::days(5),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-005".to_string(),
|
Some(5),
|
||||||
creator_id: 4,
|
"5",
|
||||||
creator_name: "Fatma Busaidy".to_string(),
|
"Fatma Busaidy",
|
||||||
title: "Digital Arts Incubator and Artwork Marketplace".to_string(),
|
"Digital Arts Incubator and Artwork Marketplace",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Active,
|
ProposalStatus::Active,
|
||||||
created_at: now - Duration::days(7),
|
now - Duration::days(7),
|
||||||
updated_at: now - Duration::days(7),
|
now - Duration::days(7),
|
||||||
voting_starts_at: Some(now - Duration::days(6)),
|
now - Duration::days(6),
|
||||||
voting_ends_at: Some(now + Duration::days(1)),
|
now + Duration::days(1),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-006".to_string(),
|
Some(6),
|
||||||
creator_id: 5,
|
"6",
|
||||||
creator_name: "Omar Makame".to_string(),
|
"Omar Makame",
|
||||||
title: "Zanzibar Renewable Energy Microgrid Network".to_string(),
|
"Zanzibar Renewable Energy Microgrid Network",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Active,
|
ProposalStatus::Active,
|
||||||
created_at: now - Duration::days(10),
|
now - Duration::days(10),
|
||||||
updated_at: now - Duration::days(9),
|
now - Duration::days(9),
|
||||||
voting_starts_at: Some(now - Duration::days(8)),
|
now - Duration::days(8),
|
||||||
voting_ends_at: Some(now + Duration::days(6)),
|
now + Duration::days(6),
|
||||||
},
|
),
|
||||||
Proposal {
|
Proposal::new(
|
||||||
id: "prop-007".to_string(),
|
Some(7),
|
||||||
creator_id: 6,
|
"7",
|
||||||
creator_name: "Saida Juma".to_string(),
|
"Saida Juma",
|
||||||
title: "ZDFZ Educational Technology Initiative".to_string(),
|
"ZDFZ Educational Technology Initiative",
|
||||||
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(),
|
"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.",
|
||||||
status: ProposalStatus::Draft,
|
ProposalStatus::Draft,
|
||||||
created_at: now - Duration::days(3),
|
now - Duration::days(3),
|
||||||
updated_at: now - Duration::days(2),
|
now - Duration::days(2),
|
||||||
voting_starts_at: None,
|
now - Duration::days(1),
|
||||||
voting_ends_at: None,
|
now + Duration::days(1),
|
||||||
},
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a mock proposal by ID
|
/// Get a mock proposal by ID
|
||||||
fn get_mock_proposal_by_id(id: &str) -> Option<Proposal> {
|
fn get_mock_proposal_by_id(id: &str) -> Option<Proposal> {
|
||||||
Self::get_mock_proposals().into_iter().find(|p| p.id == id)
|
Self::get_mock_proposals()
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.base_data.id.to_string() == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate mock votes for a specific proposal
|
/// Generate mock votes for a specific proposal
|
||||||
@ -561,7 +596,7 @@ impl GovernanceController {
|
|||||||
.filter_map(|vote| {
|
.filter_map(|vote| {
|
||||||
proposals
|
proposals
|
||||||
.iter()
|
.iter()
|
||||||
.find(|p| p.id == vote.proposal_id)
|
.find(|p| p.base_data.id.to_string() == vote.proposal_id)
|
||||||
.map(|p| (vote.clone(), p.clone()))
|
.map(|p| (vote.clone(), p.clone()))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@ -600,6 +635,8 @@ pub struct ProposalForm {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
/// Description of the proposal
|
/// Description of the proposal
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
/// Status of the proposal
|
||||||
|
pub draft: Option<bool>,
|
||||||
/// Start date for voting
|
/// Start date for voting
|
||||||
pub voting_start_date: Option<String>,
|
pub voting_start_date: Option<String>,
|
||||||
/// End date for voting
|
/// End date for voting
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use heromodels::db::hero::OurDB;
|
use heromodels::db::hero::OurDB;
|
||||||
use heromodels::{
|
use heromodels::{
|
||||||
@ -6,39 +8,70 @@ use heromodels::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// The path to the database file. Change this as needed for your environment.
|
/// The path to the database file. Change this as needed for your environment.
|
||||||
pub const DB_PATH: &str = "/tmp/ourdb_governance";
|
pub const DB_PATH: &str = "/tmp/ourdb_governance2";
|
||||||
|
|
||||||
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
|
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
|
||||||
pub fn get_db(db_path: &str) -> Result<OurDB, String> {
|
pub fn get_db(db_path: &str) -> Result<OurDB, String> {
|
||||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
let db_path = PathBuf::from(db_path);
|
||||||
|
if let Some(parent) = db_path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
// Temporarily reset the database to fix the serialization issue
|
||||||
|
let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
|
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
|
||||||
pub fn create_new_proposal(
|
pub fn create_new_proposal(
|
||||||
creator_id: &str,
|
creator_id: &str,
|
||||||
|
creator_name: &str,
|
||||||
title: &str,
|
title: &str,
|
||||||
description: &str,
|
description: &str,
|
||||||
|
status: ProposalStatus,
|
||||||
voting_start_date: Option<chrono::DateTime<Utc>>,
|
voting_start_date: Option<chrono::DateTime<Utc>>,
|
||||||
voting_end_date: Option<chrono::DateTime<Utc>>,
|
voting_end_date: Option<chrono::DateTime<Utc>>,
|
||||||
) -> Result<(u32, Proposal), String> {
|
) -> Result<(u32, Proposal), String> {
|
||||||
let db = get_db(DB_PATH).expect("Can create DB");
|
let db = get_db(DB_PATH).expect("Can create DB");
|
||||||
|
|
||||||
|
let created_at = Utc::now();
|
||||||
|
let updated_at = created_at;
|
||||||
|
|
||||||
// Create a new proposal (with auto-generated ID)
|
// Create a new proposal (with auto-generated ID)
|
||||||
let mut proposal = Proposal::new(
|
let proposal = Proposal::new(
|
||||||
None,
|
None,
|
||||||
creator_id,
|
creator_id,
|
||||||
|
creator_name,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
voting_start_date.unwrap_or_else(Utc::now),
|
voting_start_date.unwrap_or_else(Utc::now),
|
||||||
voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
|
voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
|
||||||
);
|
);
|
||||||
proposal.status = ProposalStatus::Draft;
|
|
||||||
|
|
||||||
// Save the proposal to the database
|
// Save the proposal to the database
|
||||||
let collection = db
|
let collection = db
|
||||||
.collection::<Proposal>()
|
.collection::<Proposal>()
|
||||||
.expect("can open proposal collection");
|
.expect("can open proposal collection");
|
||||||
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
|
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
|
||||||
|
|
||||||
Ok((proposal_id, saved_proposal))
|
Ok((proposal_id, saved_proposal))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads all proposals from the database and returns them as a Vec<Proposal>.
|
||||||
|
pub fn get_proposals() -> Result<Vec<Proposal>, String> {
|
||||||
|
let db = get_db(DB_PATH).map_err(|e| format!("DB error: {}", e))?;
|
||||||
|
let collection = db
|
||||||
|
.collection::<Proposal>()
|
||||||
|
.expect("can open proposal collection");
|
||||||
|
|
||||||
|
// Try to load all proposals, but handle deserialization errors gracefully
|
||||||
|
let proposals = match collection.get_all() {
|
||||||
|
Ok(props) => props,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading proposals: {:?}", e);
|
||||||
|
vec![] // Return an empty vector if there's an error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(proposals)
|
||||||
|
}
|
||||||
|
@ -37,10 +37,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between mb-3">
|
<div class="d-flex justify-content-between mb-3">
|
||||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
|
<span
|
||||||
|
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
|
||||||
{{ proposal.status }}
|
{{ proposal.status }}
|
||||||
</span>
|
</span>
|
||||||
<small class="text-muted">Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }}</small>
|
<small class="text-muted">Created by {{ proposal.creator_name }}
|
||||||
|
<!-- on {{ proposal.created_at | date(format="%Y-%m-%d") }} --></small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5>Description</h5>
|
<h5>Description</h5>
|
||||||
@ -49,10 +51,10 @@
|
|||||||
<h5>Voting Period</h5>
|
<h5>Voting Period</h5>
|
||||||
<p>
|
<p>
|
||||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||||
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
|
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
|
||||||
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Not set
|
Not set
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -71,24 +73,27 @@
|
|||||||
{% set abstain_percent = 0 %}
|
{% set abstain_percent = 0 %}
|
||||||
|
|
||||||
{% if results.total_votes > 0 %}
|
{% if results.total_votes > 0 %}
|
||||||
{% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
|
{% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
|
||||||
{% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
|
{% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
|
||||||
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
|
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
|
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
|
||||||
<div class="progress mb-3">
|
<div class="progress mb-3">
|
||||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div>
|
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
|
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
|
||||||
<div class="progress mb-3">
|
<div class="progress mb-3">
|
||||||
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div>
|
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
|
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
|
||||||
<div class="progress mb-3">
|
<div class="progress mb-3">
|
||||||
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div>
|
<div class="progress-bar bg-secondary" role="progressbar"
|
||||||
|
style="width: {{ abstain_percent }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
|
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
|
||||||
@ -106,7 +111,8 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Vote Type</label>
|
<label class="form-label">Vote Type</label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked>
|
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes"
|
||||||
|
checked>
|
||||||
<label class="form-check-label" for="voteYes">
|
<label class="form-check-label" for="voteYes">
|
||||||
Yes - I support this proposal
|
Yes - I support this proposal
|
||||||
</label>
|
</label>
|
||||||
@ -118,7 +124,8 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain" value="Abstain">
|
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
|
||||||
|
value="Abstain">
|
||||||
<label class="form-check-label" for="voteAbstain">
|
<label class="form-check-label" for="voteAbstain">
|
||||||
Abstain - I choose not to vote
|
Abstain - I choose not to vote
|
||||||
</label>
|
</label>
|
||||||
@ -126,7 +133,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="comment" class="form-label">Comment (Optional)</label>
|
<label for="comment" class="form-label">Comment (Optional)</label>
|
||||||
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="Explain your vote..."></textarea>
|
<textarea class="form-control" id="comment" name="comment" rows="3"
|
||||||
|
placeholder="Explain your vote..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
|
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
|
||||||
</form>
|
</form>
|
||||||
@ -167,7 +175,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ vote.voter_name }}</td>
|
<td>{{ vote.voter_name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
<span
|
||||||
|
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
|
||||||
{{ vote.vote_type }}
|
{{ vote.vote_type }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -3,128 +3,135 @@
|
|||||||
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Success message if present -->
|
<!-- Success message if present -->
|
||||||
{% if success %}
|
{% if success %}
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
{{ success }}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
|
||||||
<div class="row mb-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/governance">Dashboard</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" href="/governance/proposals">All Proposals</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-info alert-dismissible fade show">
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
{{ success }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
|
|
||||||
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
|
|
||||||
<div class="mt-2">
|
|
||||||
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Filter Controls -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<ul class="nav nav-tabs">
|
||||||
<div class="card-body">
|
<li class="nav-item">
|
||||||
<form action="/governance/proposals" method="get" class="row g-3">
|
<a class="nav-link" href="/governance">Dashboard</a>
|
||||||
<div class="col-md-4">
|
</li>
|
||||||
<label for="status" class="form-label">Status</label>
|
<li class="nav-item">
|
||||||
<select class="form-select" id="status" name="status">
|
<a class="nav-link active" href="/governance/proposals">All Proposals</a>
|
||||||
<option value="">All Statuses</option>
|
</li>
|
||||||
<option value="Draft">Draft</option>
|
<li class="nav-item">
|
||||||
<option value="Active">Active</option>
|
<a class="nav-link" href="/governance/my-votes">My Votes</a>
|
||||||
<option value="Approved">Approved</option>
|
</li>
|
||||||
<option value="Rejected">Rejected</option>
|
<li class="nav-item">
|
||||||
<option value="Cancelled">Cancelled</option>
|
<a class="nav-link" href="/governance/create">Create Proposal</a>
|
||||||
</select>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
<label for="search" class="form-label">Search</label>
|
</div>
|
||||||
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
|
|
||||||
</div>
|
<div class="col-12">
|
||||||
<div class="col-md-2 d-flex align-items-end">
|
<div class="alert alert-info alert-dismissible fade show">
|
||||||
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
|
||||||
</form>
|
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal
|
||||||
</div>
|
includes a detailed description, implementation plan, and voting period. Browse the list below to see all
|
||||||
</div>
|
active and past proposals.</p>
|
||||||
|
<div class="mt-2">
|
||||||
|
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i
|
||||||
|
class="bi bi-file-text"></i> Proposal Guidelines</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Proposals List -->
|
<!-- Filter Controls -->
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-body">
|
||||||
<h5 class="mb-0">All Proposals</h5>
|
<form action="/governance/proposals" method="get" class="row g-3">
|
||||||
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
|
<div class="col-md-4">
|
||||||
</div>
|
<label for="status" class="form-label">Status</label>
|
||||||
<div class="card-body">
|
<select class="form-select" id="status" name="status">
|
||||||
<div class="table-responsive">
|
<option value="">All Statuses</option>
|
||||||
<table class="table table-hover">
|
<option value="Draft">Draft</option>
|
||||||
<thead>
|
<option value="Active">Active</option>
|
||||||
<tr>
|
<option value="Approved">Approved</option>
|
||||||
<th>Title</th>
|
<option value="Rejected">Rejected</option>
|
||||||
<th>Creator</th>
|
<option value="Cancelled">Cancelled</option>
|
||||||
<th>Status</th>
|
</select>
|
||||||
<th>Created</th>
|
|
||||||
<th>Voting Period</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for proposal in proposals %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ proposal.title }}</td>
|
|
||||||
<td>{{ proposal.creator_name }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
|
||||||
{{ proposal.status }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
|
|
||||||
<td>
|
|
||||||
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
|
||||||
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
|
||||||
{% else %}
|
|
||||||
Not set
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="search" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
|
placeholder="Search by title or description">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proposals List -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">All Proposals</h5>
|
||||||
|
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Creator</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Voting Period</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for proposal in proposals %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ proposal.title }}</td>
|
||||||
|
<td>{{ proposal.creator_name }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
|
||||||
|
{{ proposal.status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
|
||||||
|
<td>
|
||||||
|
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
|
||||||
|
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{
|
||||||
|
proposal.voting_ends_at | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}
|
||||||
|
Not set
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/governance/proposals/{{ proposal.base_data.id }}"
|
||||||
|
class="btn btn-sm btn-primary">View</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user