feat: Implement Proposals page:

- Added the create new proposal functionality
- Added the list all proposals functionnality
This commit is contained in:
Mahmoud-Emad 2025-05-21 11:44:06 +03:00
parent 60198dc2d4
commit 4a2f1c7282
4 changed files with 323 additions and 237 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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>
@ -78,17 +80,20 @@
<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>

View File

@ -3,20 +3,20 @@
{% 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="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert"> <div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success }} {{ 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>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Navigation Tabs --> <!-- Navigation Tabs -->
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12"> <div class="col-12">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="nav-item"> <li class="nav-item">
@ -33,21 +33,24 @@
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="alert alert-info alert-dismissible fade show"> <div class="alert alert-info alert-dismissible fade show">
<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> <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> <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"> <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> <a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i
</div> class="bi bi-file-text"></i> Proposal Guidelines</a>
</div> </div>
</div> </div>
</div>
<!-- Filter Controls --> <!-- 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-body"> <div class="card-body">
@ -65,7 +68,8 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="search" class="form-label">Search</label> <label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description"> <input type="text" class="form-control" id="search" name="search"
placeholder="Search by title or description">
</div> </div>
<div class="col-md-2 d-flex align-items-end"> <div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Filter</button> <button type="submit" class="btn btn-primary w-100">Filter</button>
@ -74,10 +78,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Proposals List --> <!-- Proposals List -->
<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-header d-flex justify-content-between align-items-center">
@ -103,20 +107,23 @@
<td>{{ proposal.title }}</td> <td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td> <td>{{ proposal.creator_name }}</td>
<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 %}"> <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 }} {{ proposal.status }}
</span> </span>
</td> </td>
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td> <td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
<td> <td>
{% if proposal.voting_starts_at and proposal.voting_ends_at %} {% 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") }} {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{
proposal.voting_ends_at | date(format="%Y-%m-%d") }}
{% else %} {% else %}
Not set Not set
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a> <a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-primary">View</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -126,5 +133,5 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}