feat: Enhance governance dashboard with activity tracking
- Add governance activity tracker to record user actions. - Display recent activities on the governance dashboard. - Add a dedicated page to view all governance activities. - Improve header information and styling across governance pages. - Track proposal creation and voting activities.
This commit is contained in:
parent
d12a082ca1
commit
70ca9f1605
@ -1,3 +1,4 @@
|
|||||||
|
use crate::db::governance_tracker;
|
||||||
use crate::db::proposals::{self, get_proposal_by_id};
|
use crate::db::proposals::{self, get_proposal_by_id};
|
||||||
use crate::models::governance::{Vote, VoteType, VotingResults};
|
use crate::models::governance::{Vote, VoteType, VotingResults};
|
||||||
use crate::utils::render_template;
|
use crate::utils::render_template;
|
||||||
@ -81,6 +82,15 @@ impl GovernanceController {
|
|||||||
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
|
ctx.insert("active_tab", "dashboard");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "Governance Dashboard");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"Participate in community decision-making",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
// Add user to context (will always be available with our mock user)
|
// Add user to context (will always be available with our mock user)
|
||||||
let user = Self::get_user_from_session(&session).unwrap();
|
let user = Self::get_user_from_session(&session).unwrap();
|
||||||
@ -124,9 +134,14 @@ impl GovernanceController {
|
|||||||
let stats = Self::calculate_statistics_from_database(&proposals_for_stats);
|
let stats = Self::calculate_statistics_from_database(&proposals_for_stats);
|
||||||
ctx.insert("stats", &stats);
|
ctx.insert("stats", &stats);
|
||||||
|
|
||||||
// For now, we'll use empty recent activity
|
// Get recent governance activities from our tracker
|
||||||
// In a real application, this would be populated from a database
|
let recent_activity = match Self::get_recent_governance_activities() {
|
||||||
let recent_activity = Vec::<serde_json::Value>::new();
|
Ok(activities) => activities,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load recent activities: {}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
ctx.insert("recent_activity", &recent_activity);
|
ctx.insert("recent_activity", &recent_activity);
|
||||||
|
|
||||||
render_template(&tmpl, "governance/index.html", &ctx)
|
render_template(&tmpl, "governance/index.html", &ctx)
|
||||||
@ -142,6 +157,14 @@ impl GovernanceController {
|
|||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
ctx.insert("active_tab", "proposals");
|
ctx.insert("active_tab", "proposals");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "All Proposals");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"Browse and filter all governance proposals",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available
|
||||||
if let Some(user) = Self::get_user_from_session(&session) {
|
if let Some(user) = Self::get_user_from_session(&session) {
|
||||||
ctx.insert("user", &user);
|
ctx.insert("user", &user);
|
||||||
@ -206,6 +229,15 @@ impl GovernanceController {
|
|||||||
let proposal_id = path.into_inner();
|
let proposal_id = path.into_inner();
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
|
ctx.insert("active_tab", "proposals");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "Proposal Details");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"View proposal information and cast your vote",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
// Add user to context if available
|
// Add user to context if available
|
||||||
if let Some(user) = Self::get_user_from_session(&session) {
|
if let Some(user) = Self::get_user_from_session(&session) {
|
||||||
@ -259,6 +291,14 @@ impl GovernanceController {
|
|||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
ctx.insert("active_tab", "create");
|
ctx.insert("active_tab", "create");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "Create Proposal");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"Submit a new proposal for community voting",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
// Add user to context (will always be available with our mock user)
|
// Add user to context (will always be available with our mock user)
|
||||||
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);
|
||||||
@ -330,6 +370,18 @@ impl GovernanceController {
|
|||||||
"Proposal saved to DB: ID={}, title={:?}",
|
"Proposal saved to DB: ID={}, title={:?}",
|
||||||
proposal_id, saved_proposal.title
|
proposal_id, saved_proposal.title
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track the proposal creation activity
|
||||||
|
let creation_activity =
|
||||||
|
crate::models::governance::GovernanceActivity::proposal_created(
|
||||||
|
proposal_id,
|
||||||
|
&saved_proposal.title,
|
||||||
|
&user_id,
|
||||||
|
&user_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = governance_tracker::create_activity(creation_activity);
|
||||||
|
|
||||||
ctx.insert("success", "Proposal created successfully!");
|
ctx.insert("success", "Proposal created successfully!");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -401,6 +453,25 @@ impl GovernanceController {
|
|||||||
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
// Record the vote activity
|
||||||
|
let user_name = user
|
||||||
|
.get("username")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Unknown User");
|
||||||
|
|
||||||
|
// Track the vote cast activity
|
||||||
|
if let Ok(Some(proposal)) = get_proposal_by_id(proposal_id_u32) {
|
||||||
|
let vote_activity = crate::models::governance::GovernanceActivity::vote_cast(
|
||||||
|
proposal_id_u32,
|
||||||
|
&proposal.title,
|
||||||
|
user_name,
|
||||||
|
&form.vote_type,
|
||||||
|
1, // shares
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = governance_tracker::create_activity(vote_activity);
|
||||||
|
}
|
||||||
|
|
||||||
// Redirect to the proposal detail page with a success message
|
// Redirect to the proposal detail page with a success message
|
||||||
return Ok(HttpResponse::Found()
|
return Ok(HttpResponse::Found()
|
||||||
.append_header((
|
.append_header((
|
||||||
@ -422,6 +493,14 @@ impl GovernanceController {
|
|||||||
ctx.insert("active_page", "governance");
|
ctx.insert("active_page", "governance");
|
||||||
ctx.insert("active_tab", "my_votes");
|
ctx.insert("active_tab", "my_votes");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "My Votes");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"View your voting history and participation",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
// Add user to context (will always be available with our mock user)
|
// Add user to context (will always be available with our mock user)
|
||||||
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);
|
||||||
@ -463,7 +542,106 @@ impl GovernanceController {
|
|||||||
render_template(&tmpl, "governance/my_votes.html", &ctx)
|
render_template(&tmpl, "governance/my_votes.html", &ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No more mock recent activity - we're using an empty vector in the index function
|
/// Handles the all activities page route
|
||||||
|
pub async fn all_activities(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
|
||||||
|
let mut ctx = tera::Context::new();
|
||||||
|
ctx.insert("active_page", "governance");
|
||||||
|
ctx.insert("active_tab", "activities");
|
||||||
|
|
||||||
|
// Header data
|
||||||
|
ctx.insert("page_title", "All Governance Activities");
|
||||||
|
ctx.insert(
|
||||||
|
"page_description",
|
||||||
|
"Complete history of governance actions and events",
|
||||||
|
);
|
||||||
|
ctx.insert("show_create_button", &false);
|
||||||
|
|
||||||
|
// Add user to context (will always be available with our mock user)
|
||||||
|
let user = Self::get_user_from_session(&session).unwrap();
|
||||||
|
ctx.insert("user", &user);
|
||||||
|
|
||||||
|
// Get all governance activities from the database
|
||||||
|
let activities = match Self::get_all_governance_activities() {
|
||||||
|
Ok(activities) => activities,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to load all activities: {}", e);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.insert("activities", &activities);
|
||||||
|
|
||||||
|
render_template(&tmpl, "governance/all_activities.html", &ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get recent governance activities from the database
|
||||||
|
fn get_recent_governance_activities() -> Result<Vec<Value>, String> {
|
||||||
|
// Get real activities from the database (no demo data)
|
||||||
|
let activities = governance_tracker::get_recent_activities()?;
|
||||||
|
|
||||||
|
// Convert GovernanceActivity to the format expected by the template
|
||||||
|
let formatted_activities: Vec<Value> = activities
|
||||||
|
.into_iter()
|
||||||
|
.map(|activity| {
|
||||||
|
// Map activity type to appropriate icon
|
||||||
|
let (icon, action) = match activity.activity_type.as_str() {
|
||||||
|
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
|
||||||
|
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
|
||||||
|
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
|
||||||
|
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
|
||||||
|
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
|
||||||
|
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
|
||||||
|
_ => ("bi-circle-fill text-muted", "performed action"),
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::json!({
|
||||||
|
"type": activity.activity_type,
|
||||||
|
"icon": icon,
|
||||||
|
"user": activity.actor_name,
|
||||||
|
"action": action,
|
||||||
|
"proposal_title": activity.proposal_title,
|
||||||
|
"timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||||
|
"proposal_id": activity.proposal_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(formatted_activities)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all governance activities from the database
|
||||||
|
fn get_all_governance_activities() -> Result<Vec<Value>, String> {
|
||||||
|
// Get all activities from the database
|
||||||
|
let activities = governance_tracker::get_all_activities()?;
|
||||||
|
|
||||||
|
// Convert GovernanceActivity to the format expected by the template
|
||||||
|
let formatted_activities: Vec<Value> = activities
|
||||||
|
.into_iter()
|
||||||
|
.map(|activity| {
|
||||||
|
// Map activity type to appropriate icon
|
||||||
|
let (icon, action) = match activity.activity_type.as_str() {
|
||||||
|
"proposal_created" => ("bi-plus-circle-fill text-success", "created proposal"),
|
||||||
|
"vote_cast" => ("bi-check-circle-fill text-primary", "cast vote"),
|
||||||
|
"voting_started" => ("bi-play-circle-fill text-info", "started voting"),
|
||||||
|
"voting_ended" => ("bi-clock-fill text-warning", "ended voting"),
|
||||||
|
"proposal_status_changed" => ("bi-shield-check text-success", "changed status"),
|
||||||
|
"vote_option_added" => ("bi-list-ul text-secondary", "added vote option"),
|
||||||
|
_ => ("bi-circle-fill text-muted", "performed action"),
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::json!({
|
||||||
|
"type": activity.activity_type,
|
||||||
|
"icon": icon,
|
||||||
|
"user": activity.actor_name,
|
||||||
|
"action": action,
|
||||||
|
"proposal_title": activity.proposal_title,
|
||||||
|
"timestamp": activity.timestamp.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||||
|
"proposal_id": activity.proposal_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(formatted_activities)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate mock votes for a specific proposal
|
/// Generate mock votes for a specific proposal
|
||||||
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
|
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
|
||||||
|
139
actix_mvc_app/src/db/governance_tracker.rs
Normal file
139
actix_mvc_app/src/db/governance_tracker.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use crate::models::governance::GovernanceActivity;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Database path for governance activities
|
||||||
|
pub const DB_PATH: &str = "/tmp/ourdb_governance_activities";
|
||||||
|
|
||||||
|
/// Returns a shared OurDB instance for activities
|
||||||
|
pub fn get_db() -> Result<heromodels::db::hero::OurDB, String> {
|
||||||
|
let db_path = PathBuf::from(DB_PATH);
|
||||||
|
if let Some(parent) = db_path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let db = heromodels::db::hero::OurDB::new(db_path, true)
|
||||||
|
.map_err(|e| format!("Failed to create activities DB: {:?}", e))?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new governance activity and saves it to the database using OurDB
|
||||||
|
pub fn create_activity(activity: GovernanceActivity) -> Result<(u32, GovernanceActivity), String> {
|
||||||
|
let db = get_db()?;
|
||||||
|
|
||||||
|
// Since OurDB doesn't support custom models directly, we'll use a simple key-value approach
|
||||||
|
// Store each activity with a unique key and serialize it as JSON
|
||||||
|
|
||||||
|
// First, get the next available ID by checking existing keys
|
||||||
|
let activity_id = get_next_activity_id(&db)?;
|
||||||
|
|
||||||
|
// Create the activity with the assigned ID
|
||||||
|
let mut new_activity = activity;
|
||||||
|
new_activity.id = Some(activity_id);
|
||||||
|
|
||||||
|
// Serialize the activity to JSON
|
||||||
|
let activity_json = serde_json::to_string(&new_activity)
|
||||||
|
.map_err(|e| format!("Failed to serialize activity: {}", e))?;
|
||||||
|
|
||||||
|
// Store in OurDB using a key-value approach
|
||||||
|
let key = format!("activity_{}", activity_id);
|
||||||
|
|
||||||
|
// Use OurDB's raw storage capabilities to store the JSON string
|
||||||
|
// Since we can't use collections directly, we'll store as raw data
|
||||||
|
let db_path = format!("{}/{}.json", DB_PATH, key);
|
||||||
|
std::fs::write(&db_path, &activity_json)
|
||||||
|
.map_err(|e| format!("Failed to write activity to DB: {}", e))?;
|
||||||
|
|
||||||
|
// Also maintain an index of activity IDs for efficient retrieval
|
||||||
|
update_activity_index(&db, activity_id)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"✅ Activity recorded: {} - {}",
|
||||||
|
new_activity.activity_type, new_activity.description
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((activity_id, new_activity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the next available activity ID
|
||||||
|
fn get_next_activity_id(_db: &heromodels::db::hero::OurDB) -> Result<u32, String> {
|
||||||
|
let index_path = format!("{}/activity_index.json", DB_PATH);
|
||||||
|
|
||||||
|
if std::path::Path::new(&index_path).exists() {
|
||||||
|
let content = std::fs::read_to_string(&index_path)
|
||||||
|
.map_err(|e| format!("Failed to read activity index: {}", e))?;
|
||||||
|
let index: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
|
||||||
|
Ok(index.len() as u32 + 1)
|
||||||
|
} else {
|
||||||
|
Ok(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the activity index with a new activity ID
|
||||||
|
fn update_activity_index(
|
||||||
|
_db: &heromodels::db::hero::OurDB,
|
||||||
|
activity_id: u32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let index_path = format!("{}/activity_index.json", DB_PATH);
|
||||||
|
|
||||||
|
let mut index: Vec<u32> = if std::path::Path::new(&index_path).exists() {
|
||||||
|
let content = std::fs::read_to_string(&index_path)
|
||||||
|
.map_err(|e| format!("Failed to read activity index: {}", e))?;
|
||||||
|
serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
index.push(activity_id);
|
||||||
|
|
||||||
|
let content = serde_json::to_string(&index)
|
||||||
|
.map_err(|e| format!("Failed to serialize activity index: {}", e))?;
|
||||||
|
|
||||||
|
std::fs::write(&index_path, content)
|
||||||
|
.map_err(|e| format!("Failed to write activity index: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all activities from the database using OurDB
|
||||||
|
pub fn get_all_activities() -> Result<Vec<GovernanceActivity>, String> {
|
||||||
|
let _db = get_db()?;
|
||||||
|
let index_path = format!("{}/activity_index.json", DB_PATH);
|
||||||
|
|
||||||
|
// Read the activity index to get all activity IDs
|
||||||
|
if !std::path::Path::new(&index_path).exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&index_path)
|
||||||
|
.map_err(|e| format!("Failed to read activity index: {}", e))?;
|
||||||
|
let activity_ids: Vec<u32> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
|
||||||
|
|
||||||
|
let mut activities = Vec::new();
|
||||||
|
|
||||||
|
// Load each activity by ID
|
||||||
|
for activity_id in activity_ids {
|
||||||
|
let activity_path = format!("{}/activity_{}.json", DB_PATH, activity_id);
|
||||||
|
if std::path::Path::new(&activity_path).exists() {
|
||||||
|
let activity_content = std::fs::read_to_string(&activity_path)
|
||||||
|
.map_err(|e| format!("Failed to read activity {}: {}", activity_id, e))?;
|
||||||
|
|
||||||
|
if let Ok(activity) = serde_json::from_str::<GovernanceActivity>(&activity_content) {
|
||||||
|
activities.push(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(activities)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets recent activities (last 10) sorted by timestamp using OurDB
|
||||||
|
pub fn get_recent_activities() -> Result<Vec<GovernanceActivity>, String> {
|
||||||
|
let mut activities = get_all_activities()?;
|
||||||
|
|
||||||
|
// Sort by timestamp (most recent first)
|
||||||
|
activities.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||||
|
|
||||||
|
// Take only the last 10
|
||||||
|
activities.truncate(10);
|
||||||
|
|
||||||
|
Ok(activities)
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
|
pub mod governance_tracker;
|
||||||
pub mod proposals;
|
pub mod proposals;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use actix_files as fs;
|
use actix_files as fs;
|
||||||
use actix_web::{App, HttpServer, web};
|
|
||||||
use actix_web::middleware::Logger;
|
use actix_web::middleware::Logger;
|
||||||
use tera::Tera;
|
use actix_web::{App, HttpServer, web};
|
||||||
use std::io;
|
|
||||||
use std::env;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use std::env;
|
||||||
|
use std::io;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod controllers;
|
mod controllers;
|
||||||
@ -15,9 +15,9 @@ mod routes;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
// Import middleware components
|
// Import middleware components
|
||||||
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
|
use middleware::{JwtAuth, RequestTimer, SecurityHeaders};
|
||||||
use utils::redis_service;
|
|
||||||
use models::initialize_mock_data;
|
use models::initialize_mock_data;
|
||||||
|
use utils::redis_service;
|
||||||
|
|
||||||
// Initialize lazy_static for in-memory storage
|
// Initialize lazy_static for in-memory storage
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
@ -66,7 +66,8 @@ async fn main() -> io::Result<()> {
|
|||||||
let bind_address = format!("{}:{}", config.server.host, port);
|
let bind_address = format!("{}:{}", config.server.host, port);
|
||||||
|
|
||||||
// Initialize Redis client
|
// Initialize Redis client
|
||||||
let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
let redis_url =
|
||||||
|
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||||
if let Err(e) = redis_service::init_redis_client(&redis_url) {
|
if let Err(e) = redis_service::init_redis_client(&redis_url) {
|
||||||
log::error!("Failed to initialize Redis client: {}", e);
|
log::error!("Failed to initialize Redis client: {}", e);
|
||||||
log::warn!("Calendar functionality will not work properly without Redis");
|
log::warn!("Calendar functionality will not work properly without Redis");
|
||||||
@ -78,6 +79,9 @@ async fn main() -> io::Result<()> {
|
|||||||
initialize_mock_data();
|
initialize_mock_data();
|
||||||
log::info!("DeFi mock data initialized successfully");
|
log::info!("DeFi mock data initialized successfully");
|
||||||
|
|
||||||
|
// Governance activity tracker is now ready to record real user activities
|
||||||
|
log::info!("Governance activity tracker initialized and ready");
|
||||||
|
|
||||||
log::info!("Starting server at http://{}", bind_address);
|
log::info!("Starting server at http://{}", bind_address);
|
||||||
|
|
||||||
// Create and configure the HTTP server
|
// Create and configure the HTTP server
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Represents the status of a governance proposal
|
/// Represents the status of a governance proposal
|
||||||
@ -144,7 +144,13 @@ pub struct Vote {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Vote {
|
impl Vote {
|
||||||
/// Creates a new vote
|
/// Creates a new vote
|
||||||
pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option<String>) -> Self {
|
pub fn new(
|
||||||
|
proposal_id: String,
|
||||||
|
voter_id: i32,
|
||||||
|
voter_name: String,
|
||||||
|
vote_type: VoteType,
|
||||||
|
comment: Option<String>,
|
||||||
|
) -> Self {
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
Self {
|
Self {
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
@ -202,6 +208,105 @@ pub struct VotingResults {
|
|||||||
pub total_votes: usize,
|
pub total_votes: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a governance activity in the system
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GovernanceActivity {
|
||||||
|
/// Unique identifier for the activity
|
||||||
|
pub id: Option<u32>,
|
||||||
|
/// Type of activity (proposal_created, vote_cast, etc.)
|
||||||
|
pub activity_type: String,
|
||||||
|
/// ID of the related proposal
|
||||||
|
pub proposal_id: u32,
|
||||||
|
/// Title of the related proposal
|
||||||
|
pub proposal_title: String,
|
||||||
|
/// Name of the user who performed the action
|
||||||
|
pub actor_name: String,
|
||||||
|
/// Description of the activity
|
||||||
|
pub description: String,
|
||||||
|
/// Date and time when the activity occurred
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GovernanceActivity {
|
||||||
|
/// Creates a new governance activity
|
||||||
|
pub fn new(
|
||||||
|
activity_type: &str,
|
||||||
|
proposal_id: u32,
|
||||||
|
proposal_title: &str,
|
||||||
|
actor_name: &str,
|
||||||
|
description: &str,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
activity_type: activity_type.to_string(),
|
||||||
|
proposal_id,
|
||||||
|
proposal_title: proposal_title.to_string(),
|
||||||
|
actor_name: actor_name.to_string(),
|
||||||
|
description: description.to_string(),
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a proposal creation activity
|
||||||
|
pub fn proposal_created(
|
||||||
|
proposal_id: u32,
|
||||||
|
proposal_title: &str,
|
||||||
|
_creator_id: &str,
|
||||||
|
creator_name: &str,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
"proposal_created",
|
||||||
|
proposal_id,
|
||||||
|
proposal_title,
|
||||||
|
creator_name,
|
||||||
|
&format!("Proposal '{}' created by {}", proposal_title, creator_name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a vote cast activity
|
||||||
|
pub fn vote_cast(
|
||||||
|
proposal_id: u32,
|
||||||
|
proposal_title: &str,
|
||||||
|
voter_name: &str,
|
||||||
|
vote_option: &str,
|
||||||
|
shares: i64,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
"vote_cast",
|
||||||
|
proposal_id,
|
||||||
|
proposal_title,
|
||||||
|
voter_name,
|
||||||
|
&format!(
|
||||||
|
"{} voted '{}' with {} shares",
|
||||||
|
voter_name, vote_option, shares
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a proposal status change activity
|
||||||
|
pub fn proposal_status_changed(
|
||||||
|
proposal_id: u32,
|
||||||
|
proposal_title: &str,
|
||||||
|
new_status: &ProposalStatus,
|
||||||
|
reason: Option<&str>,
|
||||||
|
) -> Self {
|
||||||
|
let description = format!(
|
||||||
|
"Proposal '{}' status changed to {}{}",
|
||||||
|
proposal_title,
|
||||||
|
new_status,
|
||||||
|
reason.map(|r| format!(": {}", r)).unwrap_or_default()
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::new(
|
||||||
|
"proposal_status_changed",
|
||||||
|
proposal_id,
|
||||||
|
proposal_title,
|
||||||
|
"System",
|
||||||
|
&description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl VotingResults {
|
impl VotingResults {
|
||||||
/// Creates a new empty voting results object
|
/// Creates a new empty voting results object
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
use actix_web::web;
|
|
||||||
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
|
||||||
use crate::controllers::home::HomeController;
|
|
||||||
use crate::controllers::auth::AuthController;
|
|
||||||
use crate::controllers::ticket::TicketController;
|
|
||||||
use crate::controllers::calendar::CalendarController;
|
|
||||||
use crate::controllers::governance::GovernanceController;
|
|
||||||
use crate::controllers::flow::FlowController;
|
|
||||||
use crate::controllers::contract::ContractController;
|
|
||||||
use crate::controllers::asset::AssetController;
|
|
||||||
use crate::controllers::marketplace::MarketplaceController;
|
|
||||||
use crate::controllers::defi::DefiController;
|
|
||||||
use crate::controllers::company::CompanyController;
|
|
||||||
use crate::middleware::JwtAuth;
|
|
||||||
use crate::SESSION_KEY;
|
use crate::SESSION_KEY;
|
||||||
|
use crate::controllers::asset::AssetController;
|
||||||
|
use crate::controllers::auth::AuthController;
|
||||||
|
use crate::controllers::calendar::CalendarController;
|
||||||
|
use crate::controllers::company::CompanyController;
|
||||||
|
use crate::controllers::contract::ContractController;
|
||||||
|
use crate::controllers::defi::DefiController;
|
||||||
|
use crate::controllers::flow::FlowController;
|
||||||
|
use crate::controllers::governance::GovernanceController;
|
||||||
|
use crate::controllers::home::HomeController;
|
||||||
|
use crate::controllers::marketplace::MarketplaceController;
|
||||||
|
use crate::controllers::ticket::TicketController;
|
||||||
|
use crate::middleware::JwtAuth;
|
||||||
|
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
/// Configures all application routes
|
/// Configures all application routes
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
// Configure session middleware with the consistent key
|
// Configure session middleware with the consistent key
|
||||||
let session_middleware = SessionMiddleware::builder(
|
let session_middleware =
|
||||||
CookieSessionStore::default(),
|
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
|
||||||
SESSION_KEY.clone()
|
|
||||||
)
|
|
||||||
.cookie_secure(false) // Set to true in production with HTTPS
|
.cookie_secure(false) // Set to true in production with HTTPS
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -33,56 +31,98 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
.route("/about", web::get().to(HomeController::about))
|
.route("/about", web::get().to(HomeController::about))
|
||||||
.route("/contact", web::get().to(HomeController::contact))
|
.route("/contact", web::get().to(HomeController::contact))
|
||||||
.route("/contact", web::post().to(HomeController::submit_contact))
|
.route("/contact", web::post().to(HomeController::submit_contact))
|
||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
.route("/login", web::get().to(AuthController::login_page))
|
.route("/login", web::get().to(AuthController::login_page))
|
||||||
.route("/login", web::post().to(AuthController::login))
|
.route("/login", web::post().to(AuthController::login))
|
||||||
.route("/register", web::get().to(AuthController::register_page))
|
.route("/register", web::get().to(AuthController::register_page))
|
||||||
.route("/register", web::post().to(AuthController::register))
|
.route("/register", web::post().to(AuthController::register))
|
||||||
.route("/logout", web::get().to(AuthController::logout))
|
.route("/logout", web::get().to(AuthController::logout))
|
||||||
|
|
||||||
// Protected routes that require authentication
|
// Protected routes that require authentication
|
||||||
// These routes will be protected by the JwtAuth middleware in the main.rs file
|
// These routes will be protected by the JwtAuth middleware in the main.rs file
|
||||||
.route("/editor", web::get().to(HomeController::editor))
|
.route("/editor", web::get().to(HomeController::editor))
|
||||||
|
|
||||||
// Ticket routes
|
// Ticket routes
|
||||||
.route("/tickets", web::get().to(TicketController::list_tickets))
|
.route("/tickets", web::get().to(TicketController::list_tickets))
|
||||||
.route("/tickets/new", web::get().to(TicketController::new_ticket))
|
.route("/tickets/new", web::get().to(TicketController::new_ticket))
|
||||||
.route("/tickets", web::post().to(TicketController::create_ticket))
|
.route("/tickets", web::post().to(TicketController::create_ticket))
|
||||||
.route("/tickets/{id}", web::get().to(TicketController::show_ticket))
|
.route(
|
||||||
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
|
"/tickets/{id}",
|
||||||
.route("/tickets/{id}/status/{status}", web::post().to(TicketController::update_status))
|
web::get().to(TicketController::show_ticket),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/tickets/{id}/comment",
|
||||||
|
web::post().to(TicketController::add_comment),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/tickets/{id}/status/{status}",
|
||||||
|
web::post().to(TicketController::update_status),
|
||||||
|
)
|
||||||
.route("/my-tickets", web::get().to(TicketController::my_tickets))
|
.route("/my-tickets", web::get().to(TicketController::my_tickets))
|
||||||
|
|
||||||
// Calendar routes
|
// Calendar routes
|
||||||
.route("/calendar", web::get().to(CalendarController::calendar))
|
.route("/calendar", web::get().to(CalendarController::calendar))
|
||||||
.route("/calendar/events/new", web::get().to(CalendarController::new_event))
|
.route(
|
||||||
.route("/calendar/events", web::post().to(CalendarController::create_event))
|
"/calendar/events/new",
|
||||||
.route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
|
web::get().to(CalendarController::new_event),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/calendar/events",
|
||||||
|
web::post().to(CalendarController::create_event),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/calendar/events/{id}/delete",
|
||||||
|
web::post().to(CalendarController::delete_event),
|
||||||
|
)
|
||||||
// Governance routes
|
// Governance routes
|
||||||
.route("/governance", web::get().to(GovernanceController::index))
|
.route("/governance", web::get().to(GovernanceController::index))
|
||||||
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
|
.route(
|
||||||
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
|
"/governance/proposals",
|
||||||
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
|
web::get().to(GovernanceController::proposals),
|
||||||
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
|
)
|
||||||
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
|
.route(
|
||||||
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
|
"/governance/proposals/{id}",
|
||||||
|
web::get().to(GovernanceController::proposal_detail),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/governance/proposals/{id}/vote",
|
||||||
|
web::post().to(GovernanceController::submit_vote),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/governance/create",
|
||||||
|
web::get().to(GovernanceController::create_proposal_form),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/governance/create",
|
||||||
|
web::post().to(GovernanceController::submit_proposal),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/governance/my-votes",
|
||||||
|
web::get().to(GovernanceController::my_votes),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/governance/activities",
|
||||||
|
web::get().to(GovernanceController::all_activities),
|
||||||
|
)
|
||||||
// Flow routes
|
// Flow routes
|
||||||
.service(
|
.service(
|
||||||
web::scope("/flows")
|
web::scope("/flows")
|
||||||
.route("", web::get().to(FlowController::index))
|
.route("", web::get().to(FlowController::index))
|
||||||
.route("/list", web::get().to(FlowController::list_flows))
|
.route("/list", web::get().to(FlowController::list_flows))
|
||||||
.route("/{id}", web::get().to(FlowController::flow_detail))
|
.route("/{id}", web::get().to(FlowController::flow_detail))
|
||||||
.route("/{id}/advance", web::post().to(FlowController::advance_flow_step))
|
.route(
|
||||||
.route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck))
|
"/{id}/advance",
|
||||||
.route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step))
|
web::post().to(FlowController::advance_flow_step),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/stuck",
|
||||||
|
web::post().to(FlowController::mark_flow_step_stuck),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/step/{step_id}/log",
|
||||||
|
web::post().to(FlowController::add_log_to_flow_step),
|
||||||
|
)
|
||||||
.route("/create", web::get().to(FlowController::create_flow_form))
|
.route("/create", web::get().to(FlowController::create_flow_form))
|
||||||
.route("/create", web::post().to(FlowController::create_flow))
|
.route("/create", web::post().to(FlowController::create_flow))
|
||||||
.route("/my-flows", web::get().to(FlowController::my_flows))
|
.route("/my-flows", web::get().to(FlowController::my_flows)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Contract routes
|
// Contract routes
|
||||||
.service(
|
.service(
|
||||||
web::scope("/contracts")
|
web::scope("/contracts")
|
||||||
@ -91,9 +131,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
.route("/my", web::get().to(ContractController::my_contracts))
|
.route("/my", web::get().to(ContractController::my_contracts))
|
||||||
.route("/{id}", web::get().to(ContractController::detail))
|
.route("/{id}", web::get().to(ContractController::detail))
|
||||||
.route("/create", web::get().to(ContractController::create_form))
|
.route("/create", web::get().to(ContractController::create_form))
|
||||||
.route("/create", web::post().to(ContractController::create))
|
.route("/create", web::post().to(ContractController::create)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Asset routes
|
// Asset routes
|
||||||
.service(
|
.service(
|
||||||
web::scope("/assets")
|
web::scope("/assets")
|
||||||
@ -104,35 +143,72 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
.route("/create", web::post().to(AssetController::create))
|
.route("/create", web::post().to(AssetController::create))
|
||||||
.route("/test", web::get().to(AssetController::test))
|
.route("/test", web::get().to(AssetController::test))
|
||||||
.route("/{id}", web::get().to(AssetController::detail))
|
.route("/{id}", web::get().to(AssetController::detail))
|
||||||
.route("/{id}/valuation", web::post().to(AssetController::add_valuation))
|
.route(
|
||||||
.route("/{id}/transaction", web::post().to(AssetController::add_transaction))
|
"/{id}/valuation",
|
||||||
.route("/{id}/status/{status}", web::post().to(AssetController::update_status))
|
web::post().to(AssetController::add_valuation),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/transaction",
|
||||||
|
web::post().to(AssetController::add_transaction),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/status/{status}",
|
||||||
|
web::post().to(AssetController::update_status),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Marketplace routes
|
// Marketplace routes
|
||||||
.service(
|
.service(
|
||||||
web::scope("/marketplace")
|
web::scope("/marketplace")
|
||||||
.route("", web::get().to(MarketplaceController::index))
|
.route("", web::get().to(MarketplaceController::index))
|
||||||
.route("/listings", web::get().to(MarketplaceController::list_listings))
|
.route(
|
||||||
.route("/my", web::get().to(MarketplaceController::my_listings))
|
"/listings",
|
||||||
.route("/create", web::get().to(MarketplaceController::create_listing_form))
|
web::get().to(MarketplaceController::list_listings),
|
||||||
.route("/create", web::post().to(MarketplaceController::create_listing))
|
)
|
||||||
.route("/{id}", web::get().to(MarketplaceController::listing_detail))
|
.route("/my", web::get().to(MarketplaceController::my_listings))
|
||||||
.route("/{id}/bid", web::post().to(MarketplaceController::submit_bid))
|
.route(
|
||||||
.route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing))
|
"/create",
|
||||||
.route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing))
|
web::get().to(MarketplaceController::create_listing_form),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/create",
|
||||||
|
web::post().to(MarketplaceController::create_listing),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}",
|
||||||
|
web::get().to(MarketplaceController::listing_detail),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/bid",
|
||||||
|
web::post().to(MarketplaceController::submit_bid),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/purchase",
|
||||||
|
web::post().to(MarketplaceController::purchase_listing),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/{id}/cancel",
|
||||||
|
web::post().to(MarketplaceController::cancel_listing),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeFi routes
|
// DeFi routes
|
||||||
.service(
|
.service(
|
||||||
web::scope("/defi")
|
web::scope("/defi")
|
||||||
.route("", web::get().to(DefiController::index))
|
.route("", web::get().to(DefiController::index))
|
||||||
.route("/providing", web::post().to(DefiController::create_providing))
|
.route(
|
||||||
.route("/receiving", web::post().to(DefiController::create_receiving))
|
"/providing",
|
||||||
|
web::post().to(DefiController::create_providing),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/receiving",
|
||||||
|
web::post().to(DefiController::create_receiving),
|
||||||
|
)
|
||||||
.route("/liquidity", web::post().to(DefiController::add_liquidity))
|
.route("/liquidity", web::post().to(DefiController::add_liquidity))
|
||||||
.route("/staking", web::post().to(DefiController::create_staking))
|
.route("/staking", web::post().to(DefiController::create_staking))
|
||||||
.route("/swap", web::post().to(DefiController::swap_tokens))
|
.route("/swap", web::post().to(DefiController::swap_tokens))
|
||||||
.route("/collateral", web::post().to(DefiController::create_collateral))
|
.route(
|
||||||
|
"/collateral",
|
||||||
|
web::post().to(DefiController::create_collateral),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
// Company routes
|
// Company routes
|
||||||
.service(
|
.service(
|
||||||
@ -140,13 +216,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|||||||
.route("", web::get().to(CompanyController::index))
|
.route("", web::get().to(CompanyController::index))
|
||||||
.route("/register", web::post().to(CompanyController::register))
|
.route("/register", web::post().to(CompanyController::register))
|
||||||
.route("/view/{id}", web::get().to(CompanyController::view_company))
|
.route("/view/{id}", web::get().to(CompanyController::view_company))
|
||||||
.route("/switch/{id}", web::get().to(CompanyController::switch_entity))
|
.route(
|
||||||
)
|
"/switch/{id}",
|
||||||
|
web::get().to(CompanyController::switch_entity),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keep the /protected scope for any future routes that should be under that path
|
// Keep the /protected scope for any future routes that should be under that path
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/protected")
|
web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware
|
||||||
.wrap(JwtAuth) // Apply JWT authentication middleware
|
|
||||||
);
|
);
|
||||||
}
|
}
|
18
actix_mvc_app/src/views/governance/_header.html
Normal file
18
actix_mvc_app/src/views/governance/_header.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!-- Governance Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-1">{{ page_title }}</h1>
|
||||||
|
<p class="text-muted mb-0">{{ page_description }}</p>
|
||||||
|
</div>
|
||||||
|
{% if show_create_button %}
|
||||||
|
<div>
|
||||||
|
<a href="/governance/create" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
32
actix_mvc_app/src/views/governance/_tabs.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!-- Governance Navigation Tabs -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'dashboard' %}active{% endif %}" href="/governance">
|
||||||
|
<i class="bi bi-house"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'proposals' %}active{% endif %}" href="/governance/proposals">
|
||||||
|
<i class="bi bi-file-text"></i> All Proposals
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'create' %}active{% endif %}" href="/governance/create">
|
||||||
|
<i class="bi bi-plus-circle"></i> Create Proposal
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'my-votes' %}active{% endif %}" href="/governance/my-votes">
|
||||||
|
<i class="bi bi-check-circle"></i> My Votes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if active_tab == 'activities' %}active{% endif %}" href="/governance/activities">
|
||||||
|
<i class="bi bi-activity"></i> All Activities
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
118
actix_mvc_app/src/views/governance/all_activities.html
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}All Governance Activities{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
{% include "governance/_tabs.html" %}
|
||||||
|
|
||||||
|
<!-- Activities List -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="bi bi-activity"></i> Governance Activity History
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if activities %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50">Type</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Proposal</th>
|
||||||
|
<th width="150">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for activity in activities %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<i class="{{ activity.icon }}"></i>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ activity.user }}</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ activity.action }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/governance/proposals/{{ activity.proposal_id }}"
|
||||||
|
class="text-decoration-none">
|
||||||
|
{{ activity.proposal_title }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">
|
||||||
|
{{ activity.timestamp | date(format="%Y-%m-%d %H:%M") }}
|
||||||
|
</small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-activity display-1 text-muted"></i>
|
||||||
|
<h4 class="mt-3">No Activities Yet</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
Governance activities will appear here as users create proposals and cast votes.
|
||||||
|
</p>
|
||||||
|
<a href="/governance/create" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Create First Proposal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Activity Statistics -->
|
||||||
|
{% if activities %}
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ activities | length }}</h5>
|
||||||
|
<p class="card-text text-muted">Total Activities</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="bi bi-activity text-primary"></i>
|
||||||
|
</h5>
|
||||||
|
<p class="card-text text-muted">Activity Timeline</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="bi bi-people text-success"></i>
|
||||||
|
</h5>
|
||||||
|
<p class="card-text text-muted">Community Engagement</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -4,25 +4,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="row mb-4">
|
{% include "governance/_tabs.html" %}
|
||||||
<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" 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 active" href="/governance/create">Create Proposal</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info Alert -->
|
<!-- Info Alert -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -3,25 +3,11 @@
|
|||||||
{% block title %}Governance Dashboard{% endblock %}
|
{% block title %}Governance Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="row mb-3">
|
{% include "governance/_tabs.html" %}
|
||||||
<div class="col-12">
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" href="/governance">Dashboard</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" 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>
|
|
||||||
|
|
||||||
<!-- Info Alert -->
|
<!-- Info Alert -->
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
@ -159,7 +145,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
|
<a href="/governance/activities" class="btn btn-sm btn-outline-info">View All Activities</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,25 +3,11 @@
|
|||||||
{% block title %}My Votes - Governance Dashboard{% endblock %}
|
{% block title %}My Votes - Governance Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
<!-- Navigation Tabs -->
|
<!-- Navigation Tabs -->
|
||||||
<div class="row mb-4">
|
{% include "governance/_tabs.html" %}
|
||||||
<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" href="/governance/proposals">All Proposals</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" 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>
|
|
||||||
|
|
||||||
<!-- Info Alert -->
|
<!-- Info Alert -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -35,6 +35,12 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
{% include "governance/_tabs.html" %}
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
@ -194,7 +200,8 @@
|
|||||||
{% if proposal.status == "Active" and user and user.id %}
|
{% if proposal.status == "Active" and user and user.id %}
|
||||||
<div class="mt-auto">
|
<div class="mt-auto">
|
||||||
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
|
<h6 class="border-bottom pb-2 mb-3"><i class="bi bi-check2-square me-2"></i>Cast Your Vote</h6>
|
||||||
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post">
|
<form action="/governance/proposals/{{ proposal.base_data.id }}/vote" method="post"
|
||||||
|
id="voteForm">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex gap-2 mb-2">
|
<div class="d-flex gap-2 mb-2">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@ -243,26 +250,8 @@
|
|||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap">
|
<div class="card-header bg-light">
|
||||||
<h5 class="mb-0 mb-md-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
<h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="input-group input-group-sm me-2 d-none d-md-flex" style="width: 200px;">
|
|
||||||
<span class="input-group-text bg-white">
|
|
||||||
<i class="bi bi-search"></i>
|
|
||||||
</span>
|
|
||||||
<input type="text" class="form-control border-start-0" id="voteSearch"
|
|
||||||
placeholder="Search votes...">
|
|
||||||
</div>
|
|
||||||
<div class="btn-group" role="group" aria-label="Filter votes">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary active"
|
|
||||||
data-filter="all">All</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-success"
|
|
||||||
data-filter="yes">Yes</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" data-filter="no">No</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
||||||
data-filter="abstain">Abstain</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -372,9 +361,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Remove query parameters from URL without refreshing the page
|
// Remove query parameters from URL without refreshing the page
|
||||||
if (window.location.search.includes('vote_success=true')) {
|
if (window.location.search.includes('vote_success=true')) {
|
||||||
@ -393,46 +384,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vote filtering using data-filter attributes
|
|
||||||
const filterButtons = document.querySelectorAll('[data-filter]');
|
|
||||||
const voteRows = document.querySelectorAll('.vote-row');
|
|
||||||
const searchInput = document.getElementById('voteSearch');
|
|
||||||
|
|
||||||
// Filter votes by type
|
|
||||||
filterButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function () {
|
|
||||||
// Update active button
|
|
||||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
this.classList.add('active');
|
|
||||||
|
|
||||||
// Reset to first page and update pagination
|
|
||||||
currentPage = 1;
|
|
||||||
updatePagination();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Search functionality
|
|
||||||
if (searchInput) {
|
|
||||||
searchInput.addEventListener('input', function () {
|
|
||||||
const searchTerm = this.value.toLowerCase();
|
|
||||||
|
|
||||||
voteRows.forEach(row => {
|
|
||||||
const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
|
|
||||||
const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
|
|
||||||
|
|
||||||
if (voterName.includes(searchTerm) || comment.includes(searchTerm)) {
|
|
||||||
row.style.display = '';
|
|
||||||
} else {
|
|
||||||
row.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset pagination after search
|
|
||||||
currentPage = 1;
|
|
||||||
updatePagination();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination functionality
|
// Pagination functionality
|
||||||
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
||||||
const paginationControls = document.getElementById('paginationControls');
|
const paginationControls = document.getElementById('paginationControls');
|
||||||
@ -627,9 +578,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize pagination
|
// Initialize pagination (but don't interfere with filtering)
|
||||||
if (paginationControls) {
|
if (paginationControls) {
|
||||||
updatePagination();
|
// Only initialize pagination if there are many votes
|
||||||
|
// The filtering will handle showing/hiding rows
|
||||||
|
console.log('Pagination controls available but not interfering with filtering');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize tooltips for all elements with title attributes
|
// Initialize tooltips for all elements with title attributes
|
||||||
@ -639,8 +592,24 @@
|
|||||||
return new bootstrap.Tooltip(el);
|
return new bootstrap.Tooltip(el);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock scripts %}
|
|
||||||
|
|
||||||
{% endblock content %}
|
// Add debugging for vote form
|
||||||
|
const voteForm = document.getElementById('voteForm');
|
||||||
|
if (voteForm) {
|
||||||
|
console.log('Vote form found:', voteForm);
|
||||||
|
voteForm.addEventListener('submit', function (e) {
|
||||||
|
console.log('Vote form submitted');
|
||||||
|
const formData = new FormData(voteForm);
|
||||||
|
console.log('Form data:', Object.fromEntries(formData));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Vote form not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('Filter buttons found:', filterButtons.length);
|
||||||
|
console.log('Vote rows found:', voteRows.length);
|
||||||
|
console.log('Search input found:', searchInput ? 'Yes' : 'No');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -3,6 +3,12 @@
|
|||||||
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
{% block title %}Proposals - Governance Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Header -->
|
||||||
|
{% include "governance/_header.html" %}
|
||||||
|
|
||||||
|
<!-- Navigation Tabs -->
|
||||||
|
{% include "governance/_tabs.html" %}
|
||||||
|
|
||||||
<!-- Success message if present -->
|
<!-- Success message if present -->
|
||||||
{% if success %}
|
{% if success %}
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
@ -15,26 +21,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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-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>
|
||||||
@ -58,18 +44,23 @@
|
|||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label for="status" class="form-label">Status</label>
|
<label for="status" class="form-label">Status</label>
|
||||||
<select class="form-select" id="status" name="status">
|
<select class="form-select" id="status" name="status">
|
||||||
<option value="" {% if not status_filter or status_filter == "" %}selected{% endif %}>All Statuses</option>
|
<option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
|
||||||
<option value="Draft" {% if status_filter == "Draft" %}selected{% endif %}>Draft</option>
|
Statuses</option>
|
||||||
<option value="Active" {% if status_filter == "Active" %}selected{% endif %}>Active</option>
|
<option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
|
||||||
<option value="Approved" {% if status_filter == "Approved" %}selected{% endif %}>Approved</option>
|
<option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
|
||||||
<option value="Rejected" {% if status_filter == "Rejected" %}selected{% endif %}>Rejected</option>
|
<option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
|
||||||
<option value="Cancelled" {% if status_filter == "Cancelled" %}selected{% endif %}>Cancelled</option>
|
</option>
|
||||||
|
<option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
|
||||||
|
</option>
|
||||||
|
<option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</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"
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
placeholder="Search by title or description" value="{% if search_filter %}{{ search_filter }}{% endif %}">
|
placeholder="Search by title or description"
|
||||||
|
value="{% if search_filter %}{{ search_filter }}{% endif %}">
|
||||||
</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>
|
||||||
@ -135,7 +126,8 @@
|
|||||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||||
<h5>No proposals found</h5>
|
<h5>No proposals found</h5>
|
||||||
{% if status_filter or search_filter %}
|
{% if status_filter or search_filter %}
|
||||||
<p>No proposals match your current filter criteria. Try adjusting your filters or <a href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
|
<p>No proposals match your current filter criteria. Try adjusting your filters or <a
|
||||||
|
href="/governance/proposals" class="alert-link">view all proposals</a>.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>There are no proposals in the system yet.</p>
|
<p>There are no proposals in the system yet.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
Loading…
Reference in New Issue
Block a user