diff --git a/actix_mvc_app/src/controllers/governance.rs b/actix_mvc_app/src/controllers/governance.rs index 4762297..6536efd 100644 --- a/actix_mvc_app/src/controllers/governance.rs +++ b/actix_mvc_app/src/controllers/governance.rs @@ -1,3 +1,4 @@ +use crate::db::governance_tracker; use crate::db::proposals::{self, get_proposal_by_id}; use crate::models::governance::{Vote, VoteType, VotingResults}; use crate::utils::render_template; @@ -81,6 +82,15 @@ impl GovernanceController { pub async fn index(tmpl: web::Data, session: Session) -> Result { let mut ctx = tera::Context::new(); 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) 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); ctx.insert("stats", &stats); - // For now, we'll use empty recent activity - // In a real application, this would be populated from a database - let recent_activity = Vec::::new(); + // Get recent governance activities from our tracker + let recent_activity = match Self::get_recent_governance_activities() { + Ok(activities) => activities, + Err(e) => { + eprintln!("Failed to load recent activities: {}", e); + Vec::new() + } + }; ctx.insert("recent_activity", &recent_activity); render_template(&tmpl, "governance/index.html", &ctx) @@ -142,6 +157,14 @@ impl GovernanceController { ctx.insert("active_page", "governance"); 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 if let Some(user) = Self::get_user_from_session(&session) { ctx.insert("user", &user); @@ -206,6 +229,15 @@ impl GovernanceController { let proposal_id = path.into_inner(); let mut ctx = tera::Context::new(); 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 if let Some(user) = Self::get_user_from_session(&session) { @@ -259,6 +291,14 @@ impl GovernanceController { ctx.insert("active_page", "governance"); 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) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); @@ -330,6 +370,18 @@ impl GovernanceController { "Proposal saved to DB: ID={}, 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!"); } Err(err) => { @@ -401,6 +453,25 @@ impl GovernanceController { form.comment.as_ref().map(|s| s.to_string()), // Pass the comment from the form ) { 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 return Ok(HttpResponse::Found() .append_header(( @@ -422,6 +493,14 @@ impl GovernanceController { ctx.insert("active_page", "governance"); 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) let user = Self::get_user_from_session(&session).unwrap(); ctx.insert("user", &user); @@ -463,7 +542,106 @@ impl GovernanceController { 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, session: Session) -> Result { + 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, 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 = 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, 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 = 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 fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec { diff --git a/actix_mvc_app/src/db/governance_tracker.rs b/actix_mvc_app/src/db/governance_tracker.rs new file mode 100644 index 0000000..5ade2ae --- /dev/null +++ b/actix_mvc_app/src/db/governance_tracker.rs @@ -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 { + 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 { + 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 = 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 = 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, 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 = 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::(&activity_content) { + activities.push(activity); + } + } + } + + Ok(activities) +} + +/// Gets recent activities (last 10) sorted by timestamp using OurDB +pub fn get_recent_activities() -> Result, 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) +} diff --git a/actix_mvc_app/src/db/mod.rs b/actix_mvc_app/src/db/mod.rs index 8d18108..1b1e306 100644 --- a/actix_mvc_app/src/db/mod.rs +++ b/actix_mvc_app/src/db/mod.rs @@ -1 +1,2 @@ +pub mod governance_tracker; pub mod proposals; diff --git a/actix_mvc_app/src/main.rs b/actix_mvc_app/src/main.rs index a2e58be..76442e6 100644 --- a/actix_mvc_app/src/main.rs +++ b/actix_mvc_app/src/main.rs @@ -1,10 +1,10 @@ use actix_files as fs; -use actix_web::{App, HttpServer, web}; use actix_web::middleware::Logger; -use tera::Tera; -use std::io; -use std::env; +use actix_web::{App, HttpServer, web}; use lazy_static::lazy_static; +use std::env; +use std::io; +use tera::Tera; mod config; mod controllers; @@ -15,9 +15,9 @@ mod routes; mod utils; // Import middleware components -use middleware::{RequestTimer, SecurityHeaders, JwtAuth}; -use utils::redis_service; +use middleware::{JwtAuth, RequestTimer, SecurityHeaders}; use models::initialize_mock_data; +use utils::redis_service; // Initialize lazy_static for in-memory storage extern crate lazy_static; @@ -30,13 +30,13 @@ lazy_static! { // Create a key that's at least 64 bytes long "my_secret_session_key_that_is_at_least_64_bytes_long_for_security_reasons_1234567890abcdef".to_string() }); - + // Ensure the key is at least 64 bytes let mut key_bytes = secret.as_bytes().to_vec(); while key_bytes.len() < 64 { key_bytes.extend_from_slice(b"0123456789abcdef"); } - + actix_web::cookie::Key::from(&key_bytes[0..64]) }; } @@ -46,14 +46,14 @@ async fn main() -> io::Result<()> { // Initialize environment dotenv::dotenv().ok(); env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); - + // Load configuration let config = config::get_config(); - + // Check for port override from command line arguments let args: Vec = env::args().collect(); let mut port = config.server.port; - + for i in 1..args.len() { if args[i] == "--port" && i + 1 < args.len() { if let Ok(p) = args[i + 1].parse::() { @@ -62,24 +62,28 @@ async fn main() -> io::Result<()> { } } } - + let bind_address = format!("{}:{}", config.server.host, port); - + // 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) { log::error!("Failed to initialize Redis client: {}", e); log::warn!("Calendar functionality will not work properly without Redis"); } else { log::info!("Redis client initialized successfully"); } - + // Initialize mock data for DeFi operations initialize_mock_data(); 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); - + // Create and configure the HTTP server HttpServer::new(move || { // Initialize Tera templates @@ -90,10 +94,10 @@ async fn main() -> io::Result<()> { ::std::process::exit(1); } }; - + // Register custom Tera functions utils::register_tera_functions(&mut tera); - + App::new() // Enable logger middleware .wrap(Logger::default()) diff --git a/actix_mvc_app/src/models/governance.rs b/actix_mvc_app/src/models/governance.rs index 486bef3..34f05b2 100644 --- a/actix_mvc_app/src/models/governance.rs +++ b/actix_mvc_app/src/models/governance.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use uuid::Uuid; /// Represents the status of a governance proposal @@ -144,7 +144,13 @@ pub struct Vote { #[allow(dead_code)] impl Vote { /// Creates a new vote - pub fn new(proposal_id: String, voter_id: i32, voter_name: String, vote_type: VoteType, comment: Option) -> Self { + pub fn new( + proposal_id: String, + voter_id: i32, + voter_name: String, + vote_type: VoteType, + comment: Option, + ) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4().to_string(), @@ -202,6 +208,105 @@ pub struct VotingResults { 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, + /// 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, +} + +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)] impl VotingResults { /// Creates a new empty voting results object diff --git a/actix_mvc_app/src/routes/mod.rs b/actix_mvc_app/src/routes/mod.rs index b9810f6..d1224a9 100644 --- a/actix_mvc_app/src/routes/mod.rs +++ b/actix_mvc_app/src/routes/mod.rs @@ -1,28 +1,26 @@ -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::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 pub fn configure_routes(cfg: &mut web::ServiceConfig) { // Configure session middleware with the consistent key - let session_middleware = SessionMiddleware::builder( - CookieSessionStore::default(), - SESSION_KEY.clone() - ) - .cookie_secure(false) // Set to true in production with HTTPS - .build(); + let session_middleware = + SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone()) + .cookie_secure(false) // Set to true in production with HTTPS + .build(); // Public routes that don't require authentication cfg.service( @@ -33,56 +31,98 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/about", web::get().to(HomeController::about)) .route("/contact", web::get().to(HomeController::contact)) .route("/contact", web::post().to(HomeController::submit_contact)) - // Auth routes .route("/login", web::get().to(AuthController::login_page)) .route("/login", web::post().to(AuthController::login)) .route("/register", web::get().to(AuthController::register_page)) .route("/register", web::post().to(AuthController::register)) .route("/logout", web::get().to(AuthController::logout)) - // Protected routes that require authentication // These routes will be protected by the JwtAuth middleware in the main.rs file .route("/editor", web::get().to(HomeController::editor)) - // Ticket routes .route("/tickets", web::get().to(TicketController::list_tickets)) .route("/tickets/new", web::get().to(TicketController::new_ticket)) .route("/tickets", web::post().to(TicketController::create_ticket)) - .route("/tickets/{id}", 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( + "/tickets/{id}", + 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)) - // Calendar routes .route("/calendar", web::get().to(CalendarController::calendar)) - .route("/calendar/events/new", 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)) - + .route( + "/calendar/events/new", + 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 .route("/governance", web::get().to(GovernanceController::index)) - .route("/governance/proposals", web::get().to(GovernanceController::proposals)) - .route("/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/proposals", + web::get().to(GovernanceController::proposals), + ) + .route( + "/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 .service( web::scope("/flows") .route("", web::get().to(FlowController::index)) .route("/list", web::get().to(FlowController::list_flows)) .route("/{id}", web::get().to(FlowController::flow_detail)) - .route("/{id}/advance", 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( + "/{id}/advance", + 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::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 .service( web::scope("/contracts") @@ -91,9 +131,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/my", web::get().to(ContractController::my_contracts)) .route("/{id}", web::get().to(ContractController::detail)) .route("/create", web::get().to(ContractController::create_form)) - .route("/create", web::post().to(ContractController::create)) + .route("/create", web::post().to(ContractController::create)), ) - // Asset routes .service( web::scope("/assets") @@ -104,35 +143,72 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/create", web::post().to(AssetController::create)) .route("/test", web::get().to(AssetController::test)) .route("/{id}", web::get().to(AssetController::detail)) - .route("/{id}/valuation", 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)) + .route( + "/{id}/valuation", + 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 .service( web::scope("/marketplace") .route("", web::get().to(MarketplaceController::index)) - .route("/listings", web::get().to(MarketplaceController::list_listings)) + .route( + "/listings", + web::get().to(MarketplaceController::list_listings), + ) .route("/my", web::get().to(MarketplaceController::my_listings)) - .route("/create", 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)) + .route( + "/create", + 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 .service( web::scope("/defi") .route("", web::get().to(DefiController::index)) - .route("/providing", web::post().to(DefiController::create_providing)) - .route("/receiving", web::post().to(DefiController::create_receiving)) + .route( + "/providing", + web::post().to(DefiController::create_providing), + ) + .route( + "/receiving", + web::post().to(DefiController::create_receiving), + ) .route("/liquidity", web::post().to(DefiController::add_liquidity)) .route("/staking", web::post().to(DefiController::create_staking)) .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 .service( @@ -140,13 +216,15 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("", web::get().to(CompanyController::index)) .route("/register", web::post().to(CompanyController::register)) .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 cfg.service( - web::scope("/protected") - .wrap(JwtAuth) // Apply JWT authentication middleware + web::scope("/protected").wrap(JwtAuth), // Apply JWT authentication middleware ); -} \ No newline at end of file +} diff --git a/actix_mvc_app/src/views/governance/_header.html b/actix_mvc_app/src/views/governance/_header.html new file mode 100644 index 0000000..f82247d --- /dev/null +++ b/actix_mvc_app/src/views/governance/_header.html @@ -0,0 +1,18 @@ + +
+
+
+
+

{{ page_title }}

+

{{ page_description }}

+
+ {% if show_create_button %} + + {% endif %} +
+
+
diff --git a/actix_mvc_app/src/views/governance/_tabs.html b/actix_mvc_app/src/views/governance/_tabs.html new file mode 100644 index 0000000..4314420 --- /dev/null +++ b/actix_mvc_app/src/views/governance/_tabs.html @@ -0,0 +1,32 @@ + + diff --git a/actix_mvc_app/src/views/governance/all_activities.html b/actix_mvc_app/src/views/governance/all_activities.html new file mode 100644 index 0000000..ed5e048 --- /dev/null +++ b/actix_mvc_app/src/views/governance/all_activities.html @@ -0,0 +1,118 @@ +{% extends "base.html" %} + +{% block title %}All Governance Activities{% endblock %} + +{% block content %} +
+
+
+ + {% include "governance/_header.html" %} + + + {% include "governance/_tabs.html" %} + + +
+
+
+ Governance Activity History +
+
+
+ {% if activities %} +
+
+
+ + + + + + + + + + + + {% for activity in activities %} + + + + + + + + {% endfor %} + +
TypeUserActionProposalDate
+ + + {{ activity.user }} + + {{ activity.action }} + + + {{ activity.proposal_title }} + + + + {{ activity.timestamp | date(format="%Y-%m-%d %H:%M") }} + +
+
+
+
+ {% else %} +
+ +

No Activities Yet

+

+ Governance activities will appear here as users create proposals and cast votes. +

+ + Create First Proposal + +
+ {% endif %} +
+
+ + + {% if activities %} +
+
+
+
+
{{ activities | length }}
+

Total Activities

+
+
+
+
+
+
+
+ +
+

Activity Timeline

+
+
+
+
+
+
+
+ +
+

Community Engagement

+
+
+
+
+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/actix_mvc_app/src/views/governance/create_proposal.html b/actix_mvc_app/src/views/governance/create_proposal.html index 6636e92..dc7d446 100644 --- a/actix_mvc_app/src/views/governance/create_proposal.html +++ b/actix_mvc_app/src/views/governance/create_proposal.html @@ -4,25 +4,11 @@ {% block content %}
+ + {% include "governance/_header.html" %} + -
-
- -
-
+ {% include "governance/_tabs.html" %}
diff --git a/actix_mvc_app/src/views/governance/index.html b/actix_mvc_app/src/views/governance/index.html index ec7351e..f76db67 100644 --- a/actix_mvc_app/src/views/governance/index.html +++ b/actix_mvc_app/src/views/governance/index.html @@ -3,25 +3,11 @@ {% block title %}Governance Dashboard{% endblock %} {% block content %} + +{% include "governance/_header.html" %} + -
-
- -
-
+{% include "governance/_tabs.html" %}
@@ -159,7 +145,7 @@
diff --git a/actix_mvc_app/src/views/governance/my_votes.html b/actix_mvc_app/src/views/governance/my_votes.html index bca1cae..975d8b1 100644 --- a/actix_mvc_app/src/views/governance/my_votes.html +++ b/actix_mvc_app/src/views/governance/my_votes.html @@ -3,25 +3,11 @@ {% block title %}My Votes - Governance Dashboard{% endblock %} {% block content %} + +{% include "governance/_header.html" %} + -
-
- -
-
+{% include "governance/_tabs.html" %}
diff --git a/actix_mvc_app/src/views/governance/proposal_detail.html b/actix_mvc_app/src/views/governance/proposal_detail.html index 1ca2c23..6e57918 100644 --- a/actix_mvc_app/src/views/governance/proposal_detail.html +++ b/actix_mvc_app/src/views/governance/proposal_detail.html @@ -35,6 +35,12 @@ {% block content %}
+ + {% include "governance/_header.html" %} + + + {% include "governance/_tabs.html" %} +