WIP: development_backend #4
@@ -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<Tera>, session: Session) -> Result<impl Responder> {
 | 
			
		||||
        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::<serde_json::Value>::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<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
 | 
			
		||||
    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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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<String> = 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::<u16>() {
 | 
			
		||||
@@ -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())
 | 
			
		||||
 
 | 
			
		||||
@@ -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<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();
 | 
			
		||||
        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<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)]
 | 
			
		||||
impl VotingResults {
 | 
			
		||||
    /// Creates a new empty voting results object
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    {% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <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>
 | 
			
		||||
    {% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Info Alert -->
 | 
			
		||||
    <div class="row">
 | 
			
		||||
 
 | 
			
		||||
@@ -3,25 +3,11 @@
 | 
			
		||||
{% block title %}Governance Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
<div class="row mb-3">
 | 
			
		||||
    <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>
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Info Alert -->
 | 
			
		||||
<div class="row mb-2">
 | 
			
		||||
@@ -159,7 +145,7 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <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>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,25 +3,11 @@
 | 
			
		||||
{% block title %}My Votes - Governance Dashboard{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
    <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>
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Info Alert -->
 | 
			
		||||
<div class="row">
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,12 @@
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="container-fluid">
 | 
			
		||||
    <!-- Header -->
 | 
			
		||||
    {% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
    <!-- Navigation Tabs -->
 | 
			
		||||
    {% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
    <div class="row mb-4">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <nav aria-label="breadcrumb">
 | 
			
		||||
@@ -194,7 +200,8 @@
 | 
			
		||||
                    {% if proposal.status == "Active" and user and user.id %}
 | 
			
		||||
                    <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>
 | 
			
		||||
                        <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="d-flex gap-2 mb-2">
 | 
			
		||||
                                    <div class="form-check">
 | 
			
		||||
@@ -243,26 +250,8 @@
 | 
			
		||||
        <div class="row mt-4">
 | 
			
		||||
            <div class="col-12">
 | 
			
		||||
                <div class="card shadow-sm">
 | 
			
		||||
                    <div class="card-header bg-light d-flex justify-content-between align-items-center flex-wrap">
 | 
			
		||||
                        <h5 class="mb-0 mb-md-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 class="card-header bg-light">
 | 
			
		||||
                        <h5 class="mb-0"><i class="bi bi-list-check me-2"></i>Votes</h5>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="card-body p-0">
 | 
			
		||||
                        <div class="table-responsive">
 | 
			
		||||
@@ -372,275 +361,255 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
    {% block scripts %}
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
            // Remove query parameters from URL without refreshing the page
 | 
			
		||||
            if (window.location.search.includes('vote_success=true')) {
 | 
			
		||||
                const newUrl = window.location.pathname;
 | 
			
		||||
                window.history.replaceState({}, document.title, newUrl);
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function () {
 | 
			
		||||
        // Remove query parameters from URL without refreshing the page
 | 
			
		||||
        if (window.location.search.includes('vote_success=true')) {
 | 
			
		||||
            const newUrl = window.location.pathname;
 | 
			
		||||
            window.history.replaceState({}, document.title, newUrl);
 | 
			
		||||
 | 
			
		||||
                // Auto-hide the success alert after 5 seconds
 | 
			
		||||
                const successAlert = document.querySelector('.alert-success');
 | 
			
		||||
                if (successAlert) {
 | 
			
		||||
            // Auto-hide the success alert after 5 seconds
 | 
			
		||||
            const successAlert = document.querySelector('.alert-success');
 | 
			
		||||
            if (successAlert) {
 | 
			
		||||
                setTimeout(function () {
 | 
			
		||||
                    successAlert.classList.remove('show');
 | 
			
		||||
                    setTimeout(function () {
 | 
			
		||||
                        successAlert.classList.remove('show');
 | 
			
		||||
                        setTimeout(function () {
 | 
			
		||||
                            successAlert.remove();
 | 
			
		||||
                        }, 500);
 | 
			
		||||
                    }, 5000);
 | 
			
		||||
                }
 | 
			
		||||
                        successAlert.remove();
 | 
			
		||||
                    }, 500);
 | 
			
		||||
                }, 5000);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Pagination functionality
 | 
			
		||||
        const rowsPerPageSelect = document.getElementById('rowsPerPage');
 | 
			
		||||
        const paginationControls = document.getElementById('paginationControls');
 | 
			
		||||
        const votesTableBody = document.getElementById('votesTableBody');
 | 
			
		||||
        const startRowElement = document.getElementById('startRow');
 | 
			
		||||
        const endRowElement = document.getElementById('endRow');
 | 
			
		||||
        const totalRowsElement = document.getElementById('totalRows');
 | 
			
		||||
        const prevPageBtn = document.getElementById('prevPage');
 | 
			
		||||
        const nextPageBtn = document.getElementById('nextPage');
 | 
			
		||||
 | 
			
		||||
        let currentPage = 1;
 | 
			
		||||
        let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
 | 
			
		||||
 | 
			
		||||
        // Function to update pagination display
 | 
			
		||||
        function updatePagination() {
 | 
			
		||||
            if (!paginationControls) return;
 | 
			
		||||
 | 
			
		||||
            // Get all rows that match the current filter
 | 
			
		||||
            const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
            const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
            // Get rows that match the current filter and search term
 | 
			
		||||
            let filteredRows = Array.from(voteRows);
 | 
			
		||||
            if (filterType !== 'all') {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 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();
 | 
			
		||||
            // Apply search filter if there's a search term
 | 
			
		||||
            const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
            if (searchTerm) {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                    const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                    const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                    return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Pagination functionality
 | 
			
		||||
            const rowsPerPageSelect = document.getElementById('rowsPerPage');
 | 
			
		||||
            const paginationControls = document.getElementById('paginationControls');
 | 
			
		||||
            const votesTableBody = document.getElementById('votesTableBody');
 | 
			
		||||
            const startRowElement = document.getElementById('startRow');
 | 
			
		||||
            const endRowElement = document.getElementById('endRow');
 | 
			
		||||
            const totalRowsElement = document.getElementById('totalRows');
 | 
			
		||||
            const prevPageBtn = document.getElementById('prevPage');
 | 
			
		||||
            const nextPageBtn = document.getElementById('nextPage');
 | 
			
		||||
            const totalRows = filteredRows.length;
 | 
			
		||||
 | 
			
		||||
            let currentPage = 1;
 | 
			
		||||
            let rowsPerPage = rowsPerPageSelect ? parseInt(rowsPerPageSelect.value) : 10;
 | 
			
		||||
            // Calculate total pages
 | 
			
		||||
            const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
            // Function to update pagination display
 | 
			
		||||
            function updatePagination() {
 | 
			
		||||
                if (!paginationControls) return;
 | 
			
		||||
 | 
			
		||||
                // Get all rows that match the current filter
 | 
			
		||||
                const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
                const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
                // Get rows that match the current filter and search term
 | 
			
		||||
                let filteredRows = Array.from(voteRows);
 | 
			
		||||
                if (filterType !== 'all') {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Apply search filter if there's a search term
 | 
			
		||||
                const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
                if (searchTerm) {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                        const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                        const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                        return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const totalRows = filteredRows.length;
 | 
			
		||||
 | 
			
		||||
                // Calculate total pages
 | 
			
		||||
                const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
                // Ensure current page is valid
 | 
			
		||||
                if (currentPage > totalPages) {
 | 
			
		||||
                    currentPage = totalPages;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Update pagination controls
 | 
			
		||||
                if (paginationControls) {
 | 
			
		||||
                    // Clear existing page links (except prev/next)
 | 
			
		||||
                    const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
 | 
			
		||||
                    pageLinks.forEach(link => link.remove());
 | 
			
		||||
 | 
			
		||||
                    // Add new page links
 | 
			
		||||
                    const maxVisiblePages = 5;
 | 
			
		||||
                    let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
 | 
			
		||||
                    let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
 | 
			
		||||
 | 
			
		||||
                    // Adjust if we're near the end
 | 
			
		||||
                    if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
 | 
			
		||||
                        startPage = Math.max(1, endPage - maxVisiblePages + 1);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Insert page links before the next button
 | 
			
		||||
                    const nextPageElement = document.getElementById('nextPage');
 | 
			
		||||
                    for (let i = startPage; i <= endPage; i++) {
 | 
			
		||||
                        const li = document.createElement('li');
 | 
			
		||||
                        li.className = `page-item ${i === currentPage ? 'active' : ''}`;
 | 
			
		||||
 | 
			
		||||
                        const a = document.createElement('a');
 | 
			
		||||
                        a.className = 'page-link';
 | 
			
		||||
                        a.href = '#';
 | 
			
		||||
                        a.textContent = i;
 | 
			
		||||
                        a.addEventListener('click', function (e) {
 | 
			
		||||
                            e.preventDefault();
 | 
			
		||||
                            currentPage = i;
 | 
			
		||||
                            updatePagination();
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        li.appendChild(a);
 | 
			
		||||
                        paginationControls.insertBefore(li, nextPageElement);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Update prev/next buttons
 | 
			
		||||
                    prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
 | 
			
		||||
                    nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Show current page
 | 
			
		||||
                showCurrentPage();
 | 
			
		||||
            // Ensure current page is valid
 | 
			
		||||
            if (currentPage > totalPages) {
 | 
			
		||||
                currentPage = totalPages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Function to show current page
 | 
			
		||||
            function showCurrentPage() {
 | 
			
		||||
                if (!votesTableBody) return;
 | 
			
		||||
 | 
			
		||||
                // Get all rows that match the current filter
 | 
			
		||||
                const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
                const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
                // Get rows that match the current filter and search term
 | 
			
		||||
                let filteredRows = Array.from(voteRows);
 | 
			
		||||
                if (filterType !== 'all') {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Apply search filter if there's a search term
 | 
			
		||||
                const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
                if (searchTerm) {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                        const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                        const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                        return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Hide all rows first
 | 
			
		||||
                voteRows.forEach(row => row.style.display = 'none');
 | 
			
		||||
 | 
			
		||||
                // Calculate pagination
 | 
			
		||||
                const totalRows = filteredRows.length;
 | 
			
		||||
                const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
                // Ensure current page is valid
 | 
			
		||||
                if (currentPage > totalPages) {
 | 
			
		||||
                    currentPage = totalPages;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Show only rows for current page
 | 
			
		||||
                const start = (currentPage - 1) * rowsPerPage;
 | 
			
		||||
                const end = start + rowsPerPage;
 | 
			
		||||
 | 
			
		||||
                filteredRows.slice(start, end).forEach(row => row.style.display = '');
 | 
			
		||||
 | 
			
		||||
                // Update pagination info
 | 
			
		||||
                if (startRowElement && endRowElement && totalRowsElement) {
 | 
			
		||||
                    startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
 | 
			
		||||
                    endRowElement.textContent = Math.min(end, totalRows);
 | 
			
		||||
                    totalRowsElement.textContent = totalRows;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Event listeners for pagination
 | 
			
		||||
            if (prevPageBtn) {
 | 
			
		||||
                prevPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    if (currentPage > 1) {
 | 
			
		||||
                        currentPage--;
 | 
			
		||||
                        updatePagination();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (nextPageBtn) {
 | 
			
		||||
                nextPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    // Get all rows that match the current filter
 | 
			
		||||
                    const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
                    const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
                    // Get rows that match the current filter and search term
 | 
			
		||||
                    let filteredRows = Array.from(voteRows);
 | 
			
		||||
                    if (filterType !== 'all') {
 | 
			
		||||
                        filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Apply search filter if there's a search term
 | 
			
		||||
                    const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
                    if (searchTerm) {
 | 
			
		||||
                        filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                            const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                            const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                            return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const totalRows = filteredRows.length;
 | 
			
		||||
                    const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
                    if (currentPage < totalPages) {
 | 
			
		||||
                        currentPage++;
 | 
			
		||||
                        updatePagination();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (rowsPerPageSelect) {
 | 
			
		||||
                rowsPerPageSelect.addEventListener('change', function () {
 | 
			
		||||
                    rowsPerPage = parseInt(this.value);
 | 
			
		||||
                    currentPage = 1; // Reset to first page
 | 
			
		||||
                    updatePagination();
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Initialize pagination
 | 
			
		||||
            // Update pagination controls
 | 
			
		||||
            if (paginationControls) {
 | 
			
		||||
                updatePagination();
 | 
			
		||||
                // Clear existing page links (except prev/next)
 | 
			
		||||
                const pageLinks = paginationControls.querySelectorAll('li:not(#prevPage):not(#nextPage)');
 | 
			
		||||
                pageLinks.forEach(link => link.remove());
 | 
			
		||||
 | 
			
		||||
                // Add new page links
 | 
			
		||||
                const maxVisiblePages = 5;
 | 
			
		||||
                let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
 | 
			
		||||
                let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
 | 
			
		||||
 | 
			
		||||
                // Adjust if we're near the end
 | 
			
		||||
                if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) {
 | 
			
		||||
                    startPage = Math.max(1, endPage - maxVisiblePages + 1);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Insert page links before the next button
 | 
			
		||||
                const nextPageElement = document.getElementById('nextPage');
 | 
			
		||||
                for (let i = startPage; i <= endPage; i++) {
 | 
			
		||||
                    const li = document.createElement('li');
 | 
			
		||||
                    li.className = `page-item ${i === currentPage ? 'active' : ''}`;
 | 
			
		||||
 | 
			
		||||
                    const a = document.createElement('a');
 | 
			
		||||
                    a.className = 'page-link';
 | 
			
		||||
                    a.href = '#';
 | 
			
		||||
                    a.textContent = i;
 | 
			
		||||
                    a.addEventListener('click', function (e) {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        currentPage = i;
 | 
			
		||||
                        updatePagination();
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    li.appendChild(a);
 | 
			
		||||
                    paginationControls.insertBefore(li, nextPageElement);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Update prev/next buttons
 | 
			
		||||
                prevPageBtn.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
 | 
			
		||||
                nextPageBtn.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Initialize tooltips for all elements with title attributes
 | 
			
		||||
            const tooltipElements = document.querySelectorAll('[title]');
 | 
			
		||||
            if (tooltipElements.length > 0) {
 | 
			
		||||
                [].slice.call(tooltipElements).map(function (el) {
 | 
			
		||||
                    return new bootstrap.Tooltip(el);
 | 
			
		||||
            // Show current page
 | 
			
		||||
            showCurrentPage();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Function to show current page
 | 
			
		||||
        function showCurrentPage() {
 | 
			
		||||
            if (!votesTableBody) return;
 | 
			
		||||
 | 
			
		||||
            // Get all rows that match the current filter
 | 
			
		||||
            const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
            const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
            // Get rows that match the current filter and search term
 | 
			
		||||
            let filteredRows = Array.from(voteRows);
 | 
			
		||||
            if (filterType !== 'all') {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Apply search filter if there's a search term
 | 
			
		||||
            const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
            if (searchTerm) {
 | 
			
		||||
                filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                    const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                    const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                    return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
    {% endblock scripts %}
 | 
			
		||||
 | 
			
		||||
    {% endblock content %}
 | 
			
		||||
            // Hide all rows first
 | 
			
		||||
            voteRows.forEach(row => row.style.display = 'none');
 | 
			
		||||
 | 
			
		||||
            // Calculate pagination
 | 
			
		||||
            const totalRows = filteredRows.length;
 | 
			
		||||
            const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
            // Ensure current page is valid
 | 
			
		||||
            if (currentPage > totalPages) {
 | 
			
		||||
                currentPage = totalPages;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show only rows for current page
 | 
			
		||||
            const start = (currentPage - 1) * rowsPerPage;
 | 
			
		||||
            const end = start + rowsPerPage;
 | 
			
		||||
 | 
			
		||||
            filteredRows.slice(start, end).forEach(row => row.style.display = '');
 | 
			
		||||
 | 
			
		||||
            // Update pagination info
 | 
			
		||||
            if (startRowElement && endRowElement && totalRowsElement) {
 | 
			
		||||
                startRowElement.textContent = totalRows > 0 ? start + 1 : 0;
 | 
			
		||||
                endRowElement.textContent = Math.min(end, totalRows);
 | 
			
		||||
                totalRowsElement.textContent = totalRows;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Event listeners for pagination
 | 
			
		||||
        if (prevPageBtn) {
 | 
			
		||||
            prevPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                if (currentPage > 1) {
 | 
			
		||||
                    currentPage--;
 | 
			
		||||
                    updatePagination();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (nextPageBtn) {
 | 
			
		||||
            nextPageBtn.addEventListener('click', function (e) {
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
                // Get all rows that match the current filter
 | 
			
		||||
                const currentFilter = document.querySelector('[data-filter].active');
 | 
			
		||||
                const filterType = currentFilter ? currentFilter.getAttribute('data-filter') : 'all';
 | 
			
		||||
 | 
			
		||||
                // Get rows that match the current filter and search term
 | 
			
		||||
                let filteredRows = Array.from(voteRows);
 | 
			
		||||
                if (filterType !== 'all') {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => row.getAttribute('data-vote-type') === filterType);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Apply search filter if there's a search term
 | 
			
		||||
                const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
 | 
			
		||||
                if (searchTerm) {
 | 
			
		||||
                    filteredRows = filteredRows.filter(row => {
 | 
			
		||||
                        const voterName = row.querySelector('td:first-child').textContent.toLowerCase();
 | 
			
		||||
                        const comment = row.querySelector('td:nth-child(3)').textContent.toLowerCase();
 | 
			
		||||
                        return voterName.includes(searchTerm) || comment.includes(searchTerm);
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const totalRows = filteredRows.length;
 | 
			
		||||
                const totalPages = Math.max(1, Math.ceil(totalRows / rowsPerPage));
 | 
			
		||||
 | 
			
		||||
                if (currentPage < totalPages) {
 | 
			
		||||
                    currentPage++;
 | 
			
		||||
                    updatePagination();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (rowsPerPageSelect) {
 | 
			
		||||
            rowsPerPageSelect.addEventListener('change', function () {
 | 
			
		||||
                rowsPerPage = parseInt(this.value);
 | 
			
		||||
                currentPage = 1; // Reset to first page
 | 
			
		||||
                updatePagination();
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Initialize pagination (but don't interfere with filtering)
 | 
			
		||||
        if (paginationControls) {
 | 
			
		||||
            // 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
 | 
			
		||||
        const tooltipElements = document.querySelectorAll('[title]');
 | 
			
		||||
        if (tooltipElements.length > 0) {
 | 
			
		||||
            [].slice.call(tooltipElements).map(function (el) {
 | 
			
		||||
                return new bootstrap.Tooltip(el);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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 content %}
 | 
			
		||||
<!-- Header -->
 | 
			
		||||
{% include "governance/_header.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
{% include "governance/_tabs.html" %}
 | 
			
		||||
 | 
			
		||||
<!-- Success message if present -->
 | 
			
		||||
{% if success %}
 | 
			
		||||
<div class="row mb-4">
 | 
			
		||||
@@ -15,26 +21,6 @@
 | 
			
		||||
</div>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
<!-- Navigation Tabs -->
 | 
			
		||||
<div class="row mb-3">
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <ul class="nav nav-tabs">
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link" href="/governance">Dashboard</a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link active" href="/governance/proposals">All Proposals</a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link" href="/governance/my-votes">My Votes</a>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li class="nav-item">
 | 
			
		||||
                <a class="nav-link" href="/governance/create">Create Proposal</a>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="col-12">
 | 
			
		||||
    <div class="alert alert-info alert-dismissible fade show">
 | 
			
		||||
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
 | 
			
		||||
@@ -58,18 +44,23 @@
 | 
			
		||||
                    <div class="col-md-4">
 | 
			
		||||
                        <label for="status" class="form-label">Status</label>
 | 
			
		||||
                        <select class="form-select" id="status" name="status">
 | 
			
		||||
                            <option value="" {% if not status_filter or status_filter == "" %}selected{% endif %}>All Statuses</option>
 | 
			
		||||
                            <option value="Draft" {% if status_filter == "Draft" %}selected{% endif %}>Draft</option>
 | 
			
		||||
                            <option value="Active" {% if status_filter == "Active" %}selected{% endif %}>Active</option>
 | 
			
		||||
                            <option value="Approved" {% if status_filter == "Approved" %}selected{% endif %}>Approved</option>
 | 
			
		||||
                            <option value="Rejected" {% if status_filter == "Rejected" %}selected{% endif %}>Rejected</option>
 | 
			
		||||
                            <option value="Cancelled" {% if status_filter == "Cancelled" %}selected{% endif %}>Cancelled</option>
 | 
			
		||||
                            <option value="" {% if not status_filter or status_filter=="" %}selected{% endif %}>All
 | 
			
		||||
                                Statuses</option>
 | 
			
		||||
                            <option value="Draft" {% if status_filter=="Draft" %}selected{% endif %}>Draft</option>
 | 
			
		||||
                            <option value="Active" {% if status_filter=="Active" %}selected{% endif %}>Active</option>
 | 
			
		||||
                            <option value="Approved" {% if status_filter=="Approved" %}selected{% endif %}>Approved
 | 
			
		||||
                            </option>
 | 
			
		||||
                            <option value="Rejected" {% if status_filter=="Rejected" %}selected{% endif %}>Rejected
 | 
			
		||||
                            </option>
 | 
			
		||||
                            <option value="Cancelled" {% if status_filter=="Cancelled" %}selected{% endif %}>Cancelled
 | 
			
		||||
                            </option>
 | 
			
		||||
                        </select>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-6">
 | 
			
		||||
                        <label for="search" class="form-label">Search</label>
 | 
			
		||||
                        <input type="text" class="form-control" id="search" name="search"
 | 
			
		||||
                            placeholder="Search by title or description" value="{% if search_filter %}{{ search_filter }}{% endif %}">
 | 
			
		||||
                            placeholder="Search by title or description"
 | 
			
		||||
                            value="{% if search_filter %}{{ search_filter }}{% endif %}">
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-md-2 d-flex align-items-end">
 | 
			
		||||
                        <button type="submit" class="btn btn-primary w-100">Filter</button>
 | 
			
		||||
@@ -130,21 +121,22 @@
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </tbody>
 | 
			
		||||
                    </table>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <div class="alert alert-info text-center py-5">
 | 
			
		||||
                    <i class="bi bi-info-circle fs-1 mb-3"></i>
 | 
			
		||||
                    <h5>No proposals found</h5>
 | 
			
		||||
                    {% 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>
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <p>There are no proposals in the system yet.</p>
 | 
			
		||||
                    <div class="alert alert-info text-center py-5">
 | 
			
		||||
                        <i class="bi bi-info-circle fs-1 mb-3"></i>
 | 
			
		||||
                        <h5>No proposals found</h5>
 | 
			
		||||
                        {% 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>
 | 
			
		||||
                        {% else %}
 | 
			
		||||
                        <p>There are no proposals in the system yet.</p>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        <a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    <a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user