WIP: development_backend #4

Draft
MahmoudEmad wants to merge 26 commits from development_backend into main
42 changed files with 6263 additions and 2339 deletions

View File

@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true

286
actix_mvc_app/Cargo.lock generated
View File

@ -296,6 +296,8 @@ dependencies = [
"env_logger",
"futures",
"futures-util",
"heromodels",
"heromodels_core",
"jsonwebtoken",
"lazy_static",
"log",
@ -309,6 +311,14 @@ dependencies = [
"uuid",
]
[[package]]
name = "adapter_macros"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
]
[[package]]
name = "addr2line"
version = "0.24.2"
@ -366,6 +376,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy 0.7.35",
@ -478,6 +490,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.88"
@ -547,6 +565,26 @@ dependencies = [
"zeroize",
]
[[package]]
name = "bincode"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
dependencies = [
"bincode_derive",
"serde",
"unty",
]
[[package]]
name = "bincode_derive"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
dependencies = [
"virtue",
]
[[package]]
name = "bitflags"
version = "2.9.0"
@ -1285,12 +1323,62 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "heromodels"
version = "0.1.0"
dependencies = [
"adapter_macros",
"bincode",
"chrono",
"heromodels-derive",
"heromodels_core",
"ourdb",
"rhai",
"rhai_autobind_macros",
"rhai_client_macros",
"rhai_wrapper",
"serde",
"serde_json",
"strum",
"strum_macros",
"tst",
]
[[package]]
name = "heromodels-derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "heromodels_core"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[package]]
name = "hkdf"
version = "0.12.4"
@ -1557,6 +1645,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1756,6 +1853,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
dependencies = [
"spin",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1824,6 +1930,9 @@ name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "opaque-debug"
@ -1841,6 +1950,16 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "ourdb"
version = "0.1.0"
dependencies = [
"crc32fast",
"log",
"rand 0.8.5",
"thiserror 1.0.69",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
@ -1907,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror",
"thiserror 2.0.12",
"ucd-trie",
]
@ -2210,6 +2329,75 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rhai"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
dependencies = [
"ahash",
"bitflags",
"instant",
"no-std-compat",
"num-traits",
"once_cell",
"rhai_codegen",
"rust_decimal",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_autobind_macros"
version = "0.1.0"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_client_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rhai",
"syn",
]
[[package]]
name = "rhai_codegen"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_macros_derive"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rhai_wrapper"
version = "0.1.0"
dependencies = [
"chrono",
"rhai",
"rhai_macros_derive",
"serde",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -2247,6 +2435,16 @@ dependencies = [
"ordered-multimap",
]
[[package]]
name = "rust_decimal"
version = "1.37.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
dependencies = [
"arrayvec",
"num-traits",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -2421,7 +2619,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror",
"thiserror 2.0.12",
"time",
]
@ -2456,6 +2654,17 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smartstring"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
dependencies = [
"autocfg",
"static_assertions",
"version_check",
]
[[package]]
name = "socket2"
version = "0.4.10"
@ -2488,12 +2697,37 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -2557,13 +2791,39 @@ dependencies = [
"unic-segment",
]
[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@ -2723,6 +2983,14 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tst"
version = "0.1.0"
dependencies = [
"ourdb",
"thiserror 1.0.69",
]
[[package]]
name = "typenum"
version = "1.18.0"
@ -2831,6 +3099,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]]
name = "url"
version = "2.5.4"
@ -2888,6 +3162,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]]
name = "walkdir"
version = "2.5.0"

View File

@ -15,6 +15,8 @@ env_logger = "0.11.2"
log = "0.4.21"
dotenv = "0.15.0"
chrono = { version = "0.4.35", features = ["serde"] }
heromodels = { path = "../../db/heromodels" }
heromodels_core = { path = "../../db/heromodels_core" }
config = "0.14.0"
num_cpus = "1.16.0"
futures = "0.3.30"
@ -27,3 +29,8 @@ redis = { version = "0.23.0", features = ["tokio-comp"] }
jsonwebtoken = "8.3.0"
pulldown-cmark = "0.13.0"
urlencoding = "2.1.3"
[patch."https://git.ourworld.tf/herocode/db.git"]
rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" }
rhai_wrapper = { path = "../../rhaj/rhai_wrapper" }

View File

@ -1,6 +1,6 @@
use std::env;
use config::{Config, ConfigError, File};
use serde::Deserialize;
use std::env;
/// Application configuration
#[derive(Debug, Deserialize, Clone)]
@ -13,6 +13,7 @@ pub struct AppConfig {
/// Server configuration
#[derive(Debug, Deserialize, Clone)]
#[allow(dead_code)]
pub struct ServerConfig {
/// Host address to bind to
pub host: String,
@ -50,7 +51,8 @@ impl AppConfig {
}
// Override with environment variables (e.g., SERVER__HOST, SERVER__PORT)
config_builder = config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
config_builder =
config_builder.add_source(config::Environment::with_prefix("APP").separator("__"));
// Build and deserialize the config
let config = config_builder.build()?;

View File

@ -1,12 +1,13 @@
use actix_web::{web, HttpResponse, Result};
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use actix_web::{HttpResponse, Result, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use tera::{Context, Tera};
use crate::models::asset::{Asset, AssetType, AssetStatus, BlockchainInfo, ValuationPoint, AssetTransaction, AssetStatistics};
use crate::models::asset::{Asset, AssetStatistics, AssetStatus, AssetType, BlockchainInfo};
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct AssetForm {
pub name: String,
pub description: String,
@ -14,6 +15,7 @@ pub struct AssetForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct ValuationForm {
pub value: f64,
pub currency: String,
@ -22,6 +24,7 @@ pub struct ValuationForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct TransactionForm {
pub transaction_type: String,
pub from_address: Option<String>,
@ -80,10 +83,19 @@ impl AssetController {
.map(|asset_type| {
let mut map = serde_json::Map::new();
let type_str = asset_type.as_str();
let count = assets.iter().filter(|a| a.asset_type == *asset_type).count();
let count = assets
.iter()
.filter(|a| a.asset_type == *asset_type)
.count();
map.insert("type".to_string(), serde_json::Value::String(type_str.to_string()));
map.insert("count".to_string(), serde_json::Value::Number(serde_json::Number::from(count)));
map.insert(
"type".to_string(),
serde_json::Value::String(type_str.to_string()),
);
map.insert(
"count".to_string(),
serde_json::Value::Number(serde_json::Number::from(count)),
);
map
})
@ -106,10 +118,8 @@ impl AssetController {
let assets = Self::get_mock_assets();
println!("DEBUG: Generated {} mock assets", assets.len());
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> = assets
.iter()
.map(|a| Self::asset_to_json(a))
.collect();
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> =
assets.iter().map(|a| Self::asset_to_json(a)).collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"assets");
@ -132,10 +142,8 @@ impl AssetController {
let assets = Self::get_mock_assets();
println!("DEBUG: Generated {} mock assets", assets.len());
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> = assets
.iter()
.map(|a| Self::asset_to_json(a))
.collect();
let assets_data: Vec<serde_json::Map<String, serde_json::Value>> =
assets.iter().map(|a| Self::asset_to_json(a)).collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"assets");
@ -177,9 +185,20 @@ impl AssetController {
.iter()
.map(|v| {
let mut map = serde_json::Map::new();
map.insert("date".to_string(), serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()));
map.insert("value".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap()));
map.insert("currency".to_string(), serde_json::Value::String(v.currency.clone()));
map.insert(
"date".to_string(),
serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()),
);
map.insert(
"value".to_string(),
serde_json::Value::Number(
serde_json::Number::from_f64(v.value).unwrap(),
),
);
map.insert(
"currency".to_string(),
serde_json::Value::String(v.currency.clone()),
);
map
})
.collect();
@ -190,7 +209,7 @@ impl AssetController {
let response = render_template(&tmpl, "assets/detail.html", &context);
println!("DEBUG: Finished rendering asset detail template");
response
},
}
None => {
println!("DEBUG: Asset not found with ID {}", asset_id);
Ok(HttpResponse::NotFound().finish())
@ -216,7 +235,7 @@ impl AssetController {
("Share", "Share"),
("Bond", "Bond"),
("IntellectualProperty", "Intellectual Property"),
("Other", "Other")
("Other", "Other"),
];
context.insert("asset_types", &asset_types);
@ -237,7 +256,9 @@ impl AssetController {
// In a real application, we would save the asset to the database
// For now, we'll just redirect to the assets list
Ok(HttpResponse::Found().append_header(("Location", "/assets")).finish())
Ok(HttpResponse::Found()
.append_header(("Location", "/assets"))
.finish())
}
// Add a valuation to an asset
@ -253,7 +274,9 @@ impl AssetController {
// In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish())
Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
}
// Add a transaction to an asset
@ -269,7 +292,9 @@ impl AssetController {
// In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish())
Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
}
// Update the status of an asset
@ -284,7 +309,9 @@ impl AssetController {
// In a real application, we would update the asset in the database
// For now, we'll just redirect to the asset detail page
Ok(HttpResponse::Found().append_header(("Location", format!("/assets/{}", asset_id))).finish())
Ok(HttpResponse::Found()
.append_header(("Location", format!("/assets/{}", asset_id)))
.finish())
}
// Test method to render a simple test page
@ -325,118 +352,233 @@ impl AssetController {
fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::String(asset.id.clone()));
map.insert("name".to_string(), serde_json::Value::String(asset.name.clone()));
map.insert("description".to_string(), serde_json::Value::String(asset.description.clone()));
map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string()));
map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string()));
map.insert("owner_id".to_string(), serde_json::Value::String(asset.owner_id.clone()));
map.insert("owner_name".to_string(), serde_json::Value::String(asset.owner_name.clone()));
map.insert("created_at".to_string(), serde_json::Value::String(asset.created_at.format("%Y-%m-%d").to_string()));
map.insert("updated_at".to_string(), serde_json::Value::String(asset.updated_at.format("%Y-%m-%d").to_string()));
map.insert(
"id".to_string(),
serde_json::Value::String(asset.id.clone()),
);
map.insert(
"name".to_string(),
serde_json::Value::String(asset.name.clone()),
);
map.insert(
"description".to_string(),
serde_json::Value::String(asset.description.clone()),
);
map.insert(
"asset_type".to_string(),
serde_json::Value::String(asset.asset_type.as_str().to_string()),
);
map.insert(
"status".to_string(),
serde_json::Value::String(asset.status.as_str().to_string()),
);
map.insert(
"owner_id".to_string(),
serde_json::Value::String(asset.owner_id.clone()),
);
map.insert(
"owner_name".to_string(),
serde_json::Value::String(asset.owner_name.clone()),
);
map.insert(
"created_at".to_string(),
serde_json::Value::String(asset.created_at.format("%Y-%m-%d").to_string()),
);
map.insert(
"updated_at".to_string(),
serde_json::Value::String(asset.updated_at.format("%Y-%m-%d").to_string()),
);
// Add current valuation if available
if let Some(current_valuation) = asset.current_valuation {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(current_valuation).unwrap()));
map.insert(
"current_valuation".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(current_valuation).unwrap()),
);
if let Some(valuation_currency) = &asset.valuation_currency {
map.insert("valuation_currency".to_string(), serde_json::Value::String(valuation_currency.clone()));
map.insert(
"valuation_currency".to_string(),
serde_json::Value::String(valuation_currency.clone()),
);
}
if let Some(valuation_date) = asset.valuation_date {
map.insert("valuation_date".to_string(), serde_json::Value::String(valuation_date.format("%Y-%m-%d").to_string()));
map.insert(
"valuation_date".to_string(),
serde_json::Value::String(valuation_date.format("%Y-%m-%d").to_string()),
);
}
}
// Add blockchain info if available
if let Some(blockchain_info) = &asset.blockchain_info {
let mut blockchain_map = serde_json::Map::new();
blockchain_map.insert("blockchain".to_string(), serde_json::Value::String(blockchain_info.blockchain.clone()));
blockchain_map.insert("token_id".to_string(), serde_json::Value::String(blockchain_info.token_id.clone()));
blockchain_map.insert("contract_address".to_string(), serde_json::Value::String(blockchain_info.contract_address.clone()));
blockchain_map.insert("owner_address".to_string(), serde_json::Value::String(blockchain_info.owner_address.clone()));
blockchain_map.insert(
"blockchain".to_string(),
serde_json::Value::String(blockchain_info.blockchain.clone()),
);
blockchain_map.insert(
"token_id".to_string(),
serde_json::Value::String(blockchain_info.token_id.clone()),
);
blockchain_map.insert(
"contract_address".to_string(),
serde_json::Value::String(blockchain_info.contract_address.clone()),
);
blockchain_map.insert(
"owner_address".to_string(),
serde_json::Value::String(blockchain_info.owner_address.clone()),
);
if let Some(transaction_hash) = &blockchain_info.transaction_hash {
blockchain_map.insert("transaction_hash".to_string(), serde_json::Value::String(transaction_hash.clone()));
blockchain_map.insert(
"transaction_hash".to_string(),
serde_json::Value::String(transaction_hash.clone()),
);
}
if let Some(block_number) = blockchain_info.block_number {
blockchain_map.insert("block_number".to_string(), serde_json::Value::Number(serde_json::Number::from(block_number)));
blockchain_map.insert(
"block_number".to_string(),
serde_json::Value::Number(serde_json::Number::from(block_number)),
);
}
if let Some(timestamp) = blockchain_info.timestamp {
blockchain_map.insert("timestamp".to_string(), serde_json::Value::String(timestamp.format("%Y-%m-%d").to_string()));
blockchain_map.insert(
"timestamp".to_string(),
serde_json::Value::String(timestamp.format("%Y-%m-%d").to_string()),
);
}
map.insert("blockchain_info".to_string(), serde_json::Value::Object(blockchain_map));
map.insert(
"blockchain_info".to_string(),
serde_json::Value::Object(blockchain_map),
);
}
// Add valuation history
let valuation_history: Vec<serde_json::Value> = asset.valuation_history.iter()
let valuation_history: Vec<serde_json::Value> = asset
.valuation_history
.iter()
.map(|v| {
let mut valuation_map = serde_json::Map::new();
valuation_map.insert("id".to_string(), serde_json::Value::String(v.id.clone()));
valuation_map.insert("date".to_string(), serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()));
valuation_map.insert("value".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap()));
valuation_map.insert("currency".to_string(), serde_json::Value::String(v.currency.clone()));
valuation_map.insert("source".to_string(), serde_json::Value::String(v.source.clone()));
valuation_map.insert(
"date".to_string(),
serde_json::Value::String(v.date.format("%Y-%m-%d").to_string()),
);
valuation_map.insert(
"value".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(v.value).unwrap()),
);
valuation_map.insert(
"currency".to_string(),
serde_json::Value::String(v.currency.clone()),
);
valuation_map.insert(
"source".to_string(),
serde_json::Value::String(v.source.clone()),
);
if let Some(notes) = &v.notes {
valuation_map.insert("notes".to_string(), serde_json::Value::String(notes.clone()));
valuation_map.insert(
"notes".to_string(),
serde_json::Value::String(notes.clone()),
);
}
serde_json::Value::Object(valuation_map)
})
.collect();
map.insert("valuation_history".to_string(), serde_json::Value::Array(valuation_history));
map.insert(
"valuation_history".to_string(),
serde_json::Value::Array(valuation_history),
);
// Add transaction history
let transaction_history: Vec<serde_json::Value> = asset.transaction_history.iter()
let transaction_history: Vec<serde_json::Value> = asset
.transaction_history
.iter()
.map(|t| {
let mut transaction_map = serde_json::Map::new();
transaction_map.insert("id".to_string(), serde_json::Value::String(t.id.clone()));
transaction_map.insert("transaction_type".to_string(), serde_json::Value::String(t.transaction_type.clone()));
transaction_map.insert("date".to_string(), serde_json::Value::String(t.date.format("%Y-%m-%d").to_string()));
transaction_map.insert(
"transaction_type".to_string(),
serde_json::Value::String(t.transaction_type.clone()),
);
transaction_map.insert(
"date".to_string(),
serde_json::Value::String(t.date.format("%Y-%m-%d").to_string()),
);
if let Some(from_address) = &t.from_address {
transaction_map.insert("from_address".to_string(), serde_json::Value::String(from_address.clone()));
transaction_map.insert(
"from_address".to_string(),
serde_json::Value::String(from_address.clone()),
);
}
if let Some(to_address) = &t.to_address {
transaction_map.insert("to_address".to_string(), serde_json::Value::String(to_address.clone()));
transaction_map.insert(
"to_address".to_string(),
serde_json::Value::String(to_address.clone()),
);
}
if let Some(amount) = t.amount {
transaction_map.insert("amount".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(amount).unwrap()));
transaction_map.insert(
"amount".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(amount).unwrap()),
);
}
if let Some(currency) = &t.currency {
transaction_map.insert("currency".to_string(), serde_json::Value::String(currency.clone()));
transaction_map.insert(
"currency".to_string(),
serde_json::Value::String(currency.clone()),
);
}
if let Some(transaction_hash) = &t.transaction_hash {
transaction_map.insert("transaction_hash".to_string(), serde_json::Value::String(transaction_hash.clone()));
transaction_map.insert(
"transaction_hash".to_string(),
serde_json::Value::String(transaction_hash.clone()),
);
}
if let Some(notes) = &t.notes {
transaction_map.insert("notes".to_string(), serde_json::Value::String(notes.clone()));
transaction_map.insert(
"notes".to_string(),
serde_json::Value::String(notes.clone()),
);
}
serde_json::Value::Object(transaction_map)
})
.collect();
map.insert("transaction_history".to_string(), serde_json::Value::Array(transaction_history));
map.insert(
"transaction_history".to_string(),
serde_json::Value::Array(transaction_history),
);
// Add image URL if available
if let Some(image_url) = &asset.image_url {
map.insert("image_url".to_string(), serde_json::Value::String(image_url.clone()));
map.insert(
"image_url".to_string(),
serde_json::Value::String(image_url.clone()),
);
}
// Add external URL if available
if let Some(external_url) = &asset.external_url {
map.insert("external_url".to_string(), serde_json::Value::String(external_url.clone()));
map.insert(
"external_url".to_string(),
serde_json::Value::String(external_url.clone()),
);
}
map
@ -481,14 +623,31 @@ impl AssetController {
token_id: "ZRESORT".to_string(),
contract_address: "0x123456789abcdef123456789abcdef12345678ab".to_string(),
owner_address: "0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
transaction_hash: Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()),
transaction_hash: Some(
"0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string(),
),
block_number: Some(9876543),
timestamp: Some(now - Duration::days(120)),
});
zanzibar_resort.add_valuation(650000.0, "USD", "ZDFZ Property Registry", Some("Initial tokenization valuation".to_string()));
zanzibar_resort.add_valuation(700000.0, "USD", "International Property Appraisers", Some("Independent third-party valuation".to_string()));
zanzibar_resort.add_valuation(750000.0, "USD", "ZDFZ Property Registry", Some("Updated valuation after infrastructure improvements".to_string()));
zanzibar_resort.add_valuation(
650000.0,
"USD",
"ZDFZ Property Registry",
Some("Initial tokenization valuation".to_string()),
);
zanzibar_resort.add_valuation(
700000.0,
"USD",
"International Property Appraisers",
Some("Independent third-party valuation".to_string()),
);
zanzibar_resort.add_valuation(
750000.0,
"USD",
"ZDFZ Property Registry",
Some("Updated valuation after infrastructure improvements".to_string()),
);
zanzibar_resort.add_transaction(
"Tokenization",
@ -550,9 +709,24 @@ impl AssetController {
timestamp: Some(now - Duration::days(365)),
});
zaz_token.add_valuation(300000.0, "USD", "ZDFZ Token Exchange", Some("Initial valuation at launch".to_string()));
zaz_token.add_valuation(320000.0, "USD", "ZDFZ Token Exchange", Some("Valuation after successful governance implementation".to_string()));
zaz_token.add_valuation(350000.0, "USD", "ZDFZ Token Exchange", Some("Current market valuation".to_string()));
zaz_token.add_valuation(
300000.0,
"USD",
"ZDFZ Token Exchange",
Some("Initial valuation at launch".to_string()),
);
zaz_token.add_valuation(
320000.0,
"USD",
"ZDFZ Token Exchange",
Some("Valuation after successful governance implementation".to_string()),
);
zaz_token.add_valuation(
350000.0,
"USD",
"ZDFZ Token Exchange",
Some("Current market valuation".to_string()),
);
zaz_token.add_transaction(
"Distribution",
@ -610,14 +784,31 @@ impl AssetController {
token_id: "SPICE".to_string(),
contract_address: "0x3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
owner_address: "0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string(),
transaction_hash: Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()),
transaction_hash: Some(
"0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string(),
),
block_number: Some(7654321),
timestamp: Some(now - Duration::days(180)),
});
spice_trade_shares.add_valuation(150000.0, "USD", "ZDFZ Business Registry", Some("Initial company valuation at incorporation".to_string()));
spice_trade_shares.add_valuation(175000.0, "USD", "ZDFZ Business Registry", Some("Valuation after first export contracts".to_string()));
spice_trade_shares.add_valuation(200000.0, "USD", "ZDFZ Business Registry", Some("Current valuation after expansion to European markets".to_string()));
spice_trade_shares.add_valuation(
150000.0,
"USD",
"ZDFZ Business Registry",
Some("Initial company valuation at incorporation".to_string()),
);
spice_trade_shares.add_valuation(
175000.0,
"USD",
"ZDFZ Business Registry",
Some("Valuation after first export contracts".to_string()),
);
spice_trade_shares.add_valuation(
200000.0,
"USD",
"ZDFZ Business Registry",
Some("Current valuation after expansion to European markets".to_string()),
);
spice_trade_shares.add_transaction(
"Share Issuance",
@ -675,14 +866,31 @@ impl AssetController {
token_id: "TIDALIP".to_string(),
contract_address: "0x2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(),
owner_address: "0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string(),
transaction_hash: Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()),
transaction_hash: Some(
"0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string(),
),
block_number: Some(5432109),
timestamp: Some(now - Duration::days(120)),
});
tidal_energy_patent.add_valuation(80000.0, "USD", "ZDFZ IP Registry", Some("Initial patent valuation upon filing".to_string()));
tidal_energy_patent.add_valuation(100000.0, "USD", "ZDFZ IP Registry", Some("Valuation after successful prototype testing".to_string()));
tidal_energy_patent.add_valuation(120000.0, "USD", "ZDFZ IP Registry", Some("Current valuation after pilot implementation".to_string()));
tidal_energy_patent.add_valuation(
80000.0,
"USD",
"ZDFZ IP Registry",
Some("Initial patent valuation upon filing".to_string()),
);
tidal_energy_patent.add_valuation(
100000.0,
"USD",
"ZDFZ IP Registry",
Some("Valuation after successful prototype testing".to_string()),
);
tidal_energy_patent.add_valuation(
120000.0,
"USD",
"ZDFZ IP Registry",
Some("Current valuation after pilot implementation".to_string()),
);
tidal_energy_patent.add_transaction(
"Registration",
@ -740,14 +948,31 @@ impl AssetController {
token_id: "HERITAGE1".to_string(),
contract_address: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d".to_string(),
owner_address: "0xb794f5ea0ba39494ce839613fffba74279579268".to_string(),
transaction_hash: Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
transaction_hash: Some(
"0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string(),
),
block_number: Some(12345678),
timestamp: Some(now - Duration::days(90)),
});
zanzibar_heritage_nft.add_valuation(5000.0, "USD", "ZDFZ Artwork Marketplace", Some("Initial offering price".to_string()));
zanzibar_heritage_nft.add_valuation(5500.0, "USD", "ZDFZ Artwork Marketplace", Some("Valuation after artist exhibition".to_string()));
zanzibar_heritage_nft.add_valuation(6000.0, "USD", "ZDFZ Artwork Marketplace", Some("Current market valuation".to_string()));
zanzibar_heritage_nft.add_valuation(
5000.0,
"USD",
"ZDFZ Artwork Marketplace",
Some("Initial offering price".to_string()),
);
zanzibar_heritage_nft.add_valuation(
5500.0,
"USD",
"ZDFZ Artwork Marketplace",
Some("Valuation after artist exhibition".to_string()),
);
zanzibar_heritage_nft.add_valuation(
6000.0,
"USD",
"ZDFZ Artwork Marketplace",
Some("Current market valuation".to_string()),
);
zanzibar_heritage_nft.add_transaction(
"Minting",

View File

@ -25,6 +25,7 @@ lazy_static! {
/// Controller for handling authentication-related routes
pub struct AuthController;
#[allow(dead_code)]
impl AuthController {
/// Generate a JWT token for a user
fn generate_token(email: &str, role: &UserRole) -> Result<String, jsonwebtoken::errors::Error> {

View File

@ -1,12 +1,17 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use actix_web::{HttpResponse, Responder, Result, web};
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use tera::Tera;
use serde_json::Value;
use tera::Tera;
use crate::models::{CalendarEvent, CalendarViewMode};
use crate::utils::{RedisCalendarService, render_template};
use crate::db::calendar::{
add_event_to_calendar, create_new_event, delete_event, get_events, get_or_create_user_calendar,
};
use crate::models::CalendarViewMode;
use crate::utils::render_template;
use heromodels::models::calendar::Event;
use heromodels_core::Model;
/// Controller for handling calendar-related routes
pub struct CalendarController;
@ -14,9 +19,11 @@ pub struct CalendarController;
impl CalendarController {
/// Helper function to get user from session
fn get_user_from_session(session: &Session) -> Option<Value> {
session.get::<String>("user").ok().flatten().and_then(|user_json| {
serde_json::from_str(&user_json).ok()
})
session
.get::<String>("user")
.ok()
.flatten()
.and_then(|user_json| serde_json::from_str(&user_json).ok())
}
/// Handles the calendar page route
@ -29,13 +36,16 @@ impl CalendarController {
ctx.insert("active_page", "calendar");
// Parse the view mode from the query parameters
let view_mode = CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
let view_mode =
CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
ctx.insert("view_mode", &view_mode.to_str());
// Parse the date from the query parameters or use the current date
let date = if let Some(date_str) = &query.date {
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
Ok(naive_date) => Utc.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap()).into(),
Ok(naive_date) => Utc
.from_utc_datetime(&naive_date.and_hms_opt(0, 0, 0).unwrap())
.into(),
Err(_) => Utc::now(),
}
} else {
@ -47,44 +57,99 @@ impl CalendarController {
ctx.insert("current_month", &date.month());
ctx.insert("current_day", &date.day());
// Add user to context if available
// Add user to context if available and ensure user has a calendar
if let Some(user) = Self::get_user_from_session(&_session) {
ctx.insert("user", &user);
// Get or create user calendar
if let (Some(user_id), Some(user_name)) = (
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
user.get("full_name").and_then(|v| v.as_str()),
) {
match get_or_create_user_calendar(user_id, user_name) {
Ok(calendar) => {
log::info!(
"User calendar ready: ID {}, Name: '{}'",
calendar.get_id(),
calendar.name
);
ctx.insert("user_calendar", &calendar);
}
Err(e) => {
log::error!("Failed to get or create user calendar: {}", e);
// Continue without calendar - the app should still work
}
}
}
}
// Get events for the current view
let (start_date, end_date) = match view_mode {
CalendarViewMode::Year => {
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap();
let end = Utc
.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59)
.unwrap();
(start, end)
},
}
CalendarViewMode::Month => {
let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
let start = Utc
.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
.unwrap();
let last_day = Self::last_day_of_month(date.year(), date.month());
let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap();
let end = Utc
.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59)
.unwrap();
(start, end)
},
}
CalendarViewMode::Week => {
// Calculate the start of the week (Sunday)
let _weekday = date.weekday().num_days_from_sunday();
let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
let start_date = date
.date_naive()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap()
.pred_opt()
.unwrap();
let start = Utc.from_utc_datetime(&start_date.and_hms_opt(0, 0, 0).unwrap());
let end = start + chrono::Duration::days(7);
(start, end)
},
}
CalendarViewMode::Day => {
let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap();
let start = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
.unwrap();
let end = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59)
.unwrap();
(start, end)
},
}
};
// Get events from Redis
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
Ok(events) => events,
// Get events from database
let events = match get_events() {
Ok(db_events) => {
// Filter events for the date range
db_events
.into_iter()
.filter(|event| {
// Event overlaps with the date range
event.start_time < end_date && event.end_time > start_date
})
.collect()
}
Err(e) => {
log::error!("Failed to get events from Redis: {}", e);
log::error!("Failed to get events from database: {}", e);
vec![]
}
};
@ -94,7 +159,8 @@ impl CalendarController {
// Generate calendar data based on the view mode
match view_mode {
CalendarViewMode::Year => {
let months = (1..=12).map(|month| {
let months = (1..=12)
.map(|month| {
let month_name = match month {
1 => "January",
2 => "February",
@ -111,7 +177,8 @@ impl CalendarController {
_ => "",
};
let month_events = events.iter()
let month_events = events
.iter()
.filter(|event| {
event.start_time.month() == month || event.end_time.month() == month
})
@ -123,13 +190,16 @@ impl CalendarController {
name: month_name.to_string(),
events: month_events,
}
}).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
ctx.insert("months", &months);
},
}
CalendarViewMode::Month => {
let days_in_month = Self::last_day_of_month(date.year(), date.month());
let first_day = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
let first_day = Utc
.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0)
.unwrap();
let first_weekday = first_day.weekday().num_days_from_sunday();
let mut calendar_days = Vec::new();
@ -145,13 +215,20 @@ impl CalendarController {
// Add days for the current month
for day in 1..=days_in_month {
let day_events = events.iter()
let day_events = events
.iter()
.filter(|event| {
let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap();
let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap();
let day_start = Utc
.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0)
.unwrap();
let day_end = Utc
.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59)
.unwrap();
(event.start_time <= day_end && event.end_time >= day_start) ||
(event.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
(event.start_time <= day_end && event.end_time >= day_start)
|| (event.all_day
&& event.start_time.day() <= day
&& event.end_time.day() >= day)
})
.cloned()
.collect::<Vec<_>>();
@ -175,7 +252,7 @@ impl CalendarController {
ctx.insert("calendar_days", &calendar_days);
ctx.insert("month_name", &Self::month_name(date.month()));
},
}
CalendarViewMode::Week => {
// Calculate the start of the week (Sunday)
let weekday = date.weekday().num_days_from_sunday();
@ -184,13 +261,34 @@ impl CalendarController {
let mut week_days = Vec::new();
for i in 0..7 {
let day_date = week_start + chrono::Duration::days(i);
let day_events = events.iter()
let day_events = events
.iter()
.filter(|event| {
let day_start = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 0, 0, 0).unwrap();
let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap();
let day_start = Utc
.with_ymd_and_hms(
day_date.year(),
day_date.month(),
day_date.day(),
0,
0,
0,
)
.unwrap();
let day_end = Utc
.with_ymd_and_hms(
day_date.year(),
day_date.month(),
day_date.day(),
23,
59,
59,
)
.unwrap();
(event.start_time <= day_end && event.end_time >= day_start) ||
(event.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day())
(event.start_time <= day_end && event.end_time >= day_start)
|| (event.all_day
&& event.start_time.day() <= day_date.day()
&& event.end_time.day() >= day_date.day())
})
.cloned()
.collect::<Vec<_>>();
@ -203,16 +301,22 @@ impl CalendarController {
}
ctx.insert("week_days", &week_days);
},
}
CalendarViewMode::Day => {
log::info!("Day view selected");
ctx.insert("day_name", &Self::day_name(date.weekday().num_days_from_sunday()));
ctx.insert(
"day_name",
&Self::day_name(date.weekday().num_days_from_sunday()),
);
// Add debug info
log::info!("Events count: {}", events.len());
log::info!("Current date: {}", date.format("%Y-%m-%d"));
log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday()));
},
log::info!(
"Day name: {}",
Self::day_name(date.weekday().num_days_from_sunday())
);
}
}
render_template(&tmpl, "calendar/index.html", &ctx)
@ -223,9 +327,24 @@ impl CalendarController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar");
// Add user to context if available
// Add user to context if available and ensure user has a calendar
if let Some(user) = Self::get_user_from_session(&_session) {
ctx.insert("user", &user);
// Get or create user calendar
if let (Some(user_id), Some(user_name)) = (
user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32),
user.get("full_name").and_then(|v| v.as_str()),
) {
match get_or_create_user_calendar(user_id, user_name) {
Ok(calendar) => {
ctx.insert("user_calendar", &calendar);
}
Err(e) => {
log::error!("Failed to get or create user calendar: {}", e);
}
}
}
}
render_template(&tmpl, "calendar/new_event.html", &ctx)
@ -237,44 +356,91 @@ impl CalendarController {
tmpl: web::Data<Tera>,
_session: Session,
) -> Result<impl Responder> {
// Log the form data for debugging
log::info!(
"Creating event with form data: title='{}', start_time='{}', end_time='{}', all_day={}",
form.title,
form.start_time,
form.end_time,
form.all_day
);
// Parse the start and end times
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
Ok(dt) => dt.with_timezone(&Utc),
Err(e) => {
log::error!("Failed to parse start time: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
log::error!("Failed to parse start time '{}': {}", form.start_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid start time format"));
}
};
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
Ok(dt) => dt.with_timezone(&Utc),
Err(e) => {
log::error!("Failed to parse end time: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
log::error!("Failed to parse end time '{}': {}", form.end_time, e);
return Ok(HttpResponse::BadRequest().body("Invalid end time format"));
}
};
// Create the event
let event = CalendarEvent::new(
form.title.clone(),
form.description.clone(),
// Get user information from session
let user_info = Self::get_user_from_session(&_session);
let (user_id, user_name) = if let Some(user) = &user_info {
let id = user.get("id").and_then(|v| v.as_u64()).map(|v| v as u32);
let name = user
.get("full_name")
.and_then(|v| v.as_str())
.unwrap_or("Unknown User");
log::info!("User from session: id={:?}, name='{}'", id, name);
(id, name)
} else {
log::warn!("No user found in session");
(None, "Unknown User")
};
// Create the event in the database
match create_new_event(
&form.title,
Some(&form.description),
start_time,
end_time,
Some(form.color.clone()),
None, // location
Some(&form.color),
form.all_day,
None, // User ID would come from session in a real app
);
user_id,
None, // category
None, // reminder_minutes
) {
Ok((event_id, _saved_event)) => {
log::info!("Created event with ID: {}", event_id);
// Save the event to Redis
match RedisCalendarService::save_event(&event) {
// If user is logged in, add the event to their calendar
if let Some(user_id) = user_id {
match get_or_create_user_calendar(user_id, user_name) {
Ok(calendar) => match add_event_to_calendar(calendar.get_id(), event_id) {
Ok(_) => {
log::info!(
"Added event {} to calendar {}",
event_id,
calendar.get_id()
);
}
Err(e) => {
log::error!("Failed to add event to calendar: {}", e);
}
},
Err(e) => {
log::error!("Failed to get user calendar: {}", e);
}
}
}
// Redirect to the calendar page
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar"))
.finish())
},
}
Err(e) => {
log::error!("Failed to save event to Redis: {}", e);
log::error!("Failed to save event to database: {}", e);
// Show an error message
let mut ctx = tera::Context::new();
@ -282,13 +448,15 @@ impl CalendarController {
ctx.insert("error", "Failed to save event");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&_session) {
if let Some(user) = user_info {
ctx.insert("user", &user);
}
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
Ok(HttpResponse::InternalServerError()
.content_type("text/html")
.body(result.into_body()))
}
}
}
@ -300,16 +468,26 @@ impl CalendarController {
) -> Result<impl Responder> {
let id = path.into_inner();
// Delete the event from Redis
match RedisCalendarService::delete_event(&id) {
// Parse the event ID
let event_id = match id.parse::<u32>() {
Ok(id) => id,
Err(_) => {
log::error!("Invalid event ID: {}", id);
return Ok(HttpResponse::BadRequest().body("Invalid event ID"));
}
};
// Delete the event from database
match delete_event(event_id) {
Ok(_) => {
log::info!("Deleted event with ID: {}", event_id);
// Redirect to the calendar page
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar"))
.finish())
},
}
Err(e) => {
log::error!("Failed to delete event from Redis: {}", e);
log::error!("Failed to delete event from database: {}", e);
Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
}
}
@ -326,7 +504,7 @@ impl CalendarController {
} else {
28
}
},
}
_ => 30, // Default to 30 days
}
}
@ -387,7 +565,7 @@ pub struct EventForm {
#[derive(Debug, Serialize)]
struct CalendarDay {
day: u32,
events: Vec<CalendarEvent>,
events: Vec<Event>,
is_current_month: bool,
}
@ -396,5 +574,5 @@ struct CalendarDay {
struct CalendarMonth {
month: u32,
name: String,
events: Vec<CalendarEvent>,
events: Vec<Event>,
}

View File

@ -1,12 +1,12 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_web::HttpRequest;
use tera::{Context, Tera};
use serde::Deserialize;
use chrono::Utc;
use crate::utils::render_template;
use actix_web::HttpRequest;
use actix_web::{HttpResponse, Result, web};
use serde::Deserialize;
use tera::{Context, Tera};
// Form structs for company operations
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CompanyRegistrationForm {
pub company_name: String,
pub company_type: String,
@ -32,7 +32,9 @@ impl CompanyController {
// Check for success message
if let Some(pos) = query_string.find("success=") {
let start = pos + 8; // length of "success="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let end = query_string[start..]
.find('&')
.map_or(query_string.len(), |e| e + start);
let success = &query_string[start..end];
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
context.insert("success", &decoded);
@ -41,16 +43,21 @@ impl CompanyController {
// Check for entity context
if let Some(pos) = query_string.find("entity=") {
let start = pos + 7; // length of "entity="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let end = query_string[start..]
.find('&')
.map_or(query_string.len(), |e| e + start);
let entity = &query_string[start..end];
context.insert("entity", &entity);
// Also get entity name if present
if let Some(pos) = query_string.find("entity_name=") {
let start = pos + 12; // length of "entity_name="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let end = query_string[start..]
.find('&')
.map_or(query_string.len(), |e| e + start);
let entity_name = &query_string[start..end];
let decoded_name = urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
let decoded_name =
urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
context.insert("entity_name", &decoded_name);
println!("DEBUG: Entity context set to {} ({})", entity, decoded_name);
}
@ -63,7 +70,10 @@ impl CompanyController {
}
// View company details
pub async fn view_company(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
pub async fn view_company(
tmpl: web::Data<Tera>,
path: web::Path<String>,
) -> Result<HttpResponse> {
let company_id = path.into_inner();
let mut context = Context::new();
@ -87,10 +97,7 @@ impl CompanyController {
context.insert("payment_method", &"Credit Card (****4582)");
// Shareholders data
let shareholders = vec![
("John Smith", "60%"),
("Sarah Johnson", "40%"),
];
let shareholders = vec![("John Smith", "60%"), ("Sarah Johnson", "40%")];
context.insert("shareholders", &shareholders);
// Contracts data
@ -100,7 +107,7 @@ impl CompanyController {
("Digital Asset Issuance", "Signed"),
];
context.insert("contracts", &contracts);
},
}
"company2" => {
context.insert("company_name", &"Blockchain Innovations Ltd");
context.insert("company_type", &"Growth FZC");
@ -127,7 +134,7 @@ impl CompanyController {
("Physical Asset Holding", "Signed"),
];
context.insert("contracts", &contracts);
},
}
"company3" => {
context.insert("company_name", &"Sustainable Energy Cooperative");
context.insert("company_type", &"Cooperative FZC");
@ -153,7 +160,7 @@ impl CompanyController {
("Cooperative Governance", "Pending"),
];
context.insert("contracts", &contracts);
},
}
_ => {
// If company_id is not recognized, redirect to company index
return Ok(HttpResponse::Found()
@ -179,7 +186,7 @@ impl CompanyController {
"company1" => "Zanzibar Digital Solutions",
"company2" => "Blockchain Innovations Ltd",
"company3" => "Sustainable Energy Cooperative",
_ => "Unknown Company"
_ => "Unknown Company",
};
// In a real application, we would set a session/cookie for the current entity
@ -188,16 +195,21 @@ impl CompanyController {
let encoded_message = urlencoding::encode(&success_message);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/company?success={}&entity={}&entity_name={}",
encoded_message, company_id, urlencoding::encode(company_name))))
.append_header((
"Location",
format!(
"/company?success={}&entity={}&entity_name={}",
encoded_message,
company_id,
urlencoding::encode(company_name)
),
))
.finish())
}
// Process company registration
pub async fn register(
mut form: actix_multipart::Multipart,
) -> Result<HttpResponse> {
use actix_web::{http::header};
pub async fn register(mut form: actix_multipart::Multipart) -> Result<HttpResponse> {
use actix_web::http::header;
use futures_util::stream::StreamExt as _;
use std::collections::HashMap;
@ -220,7 +232,10 @@ impl CompanyController {
if name == "company_docs" {
files.push(value); // Just collect files in memory for now
} else {
fields.insert(name.to_string(), String::from_utf8_lossy(&value).to_string());
fields.insert(
name.to_string(),
String::from_utf8_lossy(&value).to_string(),
);
}
}
}
@ -231,15 +246,26 @@ impl CompanyController {
let shareholders = fields.get("shareholders").cloned().unwrap_or_default();
// Log received fields (mock DB insert)
println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
company_name, company_type, shareholders, files.len());
println!(
"[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
company_name,
company_type,
shareholders,
files.len()
);
// Create success message
let success_message = format!("Successfully registered {} as a {}", company_name, company_type);
let success_message = format!(
"Successfully registered {} as a {}",
company_name, company_type
);
// Redirect back to /company with success message
Ok(HttpResponse::SeeOther()
.append_header((header::LOCATION, format!("/company?success={}", urlencoding::encode(&success_message))))
.append_header((
header::LOCATION,
format!("/company?success={}", urlencoding::encode(&success_message)),
))
.finish())
}
}

View File

@ -1,15 +1,18 @@
use actix_web::{web, HttpResponse, Result, Error};
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use serde::Deserialize;
use serde_json::json;
use actix_web::web::Query;
use actix_web::{Error, HttpResponse, Result, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use std::collections::HashMap;
use tera::{Context, Tera};
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, TocItem};
use crate::models::contract::{
Contract, ContractRevision, ContractSigner, ContractStatistics, ContractStatus, ContractType,
SignerStatus, TocItem,
};
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct ContractForm {
pub title: String,
pub description: String,
@ -18,6 +21,7 @@ pub struct ContractForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct SignerForm {
pub name: String,
pub email: String,
@ -49,7 +53,8 @@ impl ContractController {
context.insert("recent_contracts", &recent_contracts);
// Add pending signature contracts
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> =
contracts
.iter()
.filter(|c| c.status == ContractStatus::PendingSignatures)
.map(|c| Self::contract_to_json(c))
@ -110,7 +115,7 @@ impl ContractController {
pub async fn detail(
tmpl: web::Data<Tera>,
path: web::Path<String>,
query: Query<HashMap<String, String>>
query: Query<HashMap<String, String>>,
) -> Result<HttpResponse, Error> {
let contract_id = path.into_inner();
let mut context = Context::new();
@ -137,10 +142,13 @@ impl ContractController {
context.insert("contract", &contract_json);
// If this contract uses multi-page markdown, load the selected section
println!("DEBUG: content_dir = {:?}, toc = {:?}", contract.content_dir, contract.toc);
println!(
"DEBUG: content_dir = {:?}, toc = {:?}",
contract.content_dir, contract.toc
);
if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) {
use pulldown_cmark::{Options, Parser, html};
use std::fs;
use pulldown_cmark::{Parser, Options, html};
// Helper to flatten toc recursively
fn flatten_toc<'a>(items: &'a Vec<TocItem>, out: &mut Vec<&'a TocItem>) {
for item in items {
@ -154,15 +162,28 @@ impl ContractController {
flatten_toc(&toc, &mut flat_toc);
let section_param = query.get("section");
let selected_file = section_param
.and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone()))
.unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default());
.and_then(|f| {
flat_toc
.iter()
.find(|item| item.file == *f)
.map(|item| item.file.clone())
})
.unwrap_or_else(|| {
flat_toc
.get(0)
.map(|item| item.file.clone())
.unwrap_or_default()
});
context.insert("section", &selected_file);
let rel_path = format!("{}/{}", content_dir, selected_file);
let abs_path = match std::env::current_dir() {
Ok(dir) => dir.join(&rel_path),
Err(_) => std::path::PathBuf::from(&rel_path),
};
println!("DEBUG: Attempting to read markdown file at absolute path: {:?}", abs_path);
println!(
"DEBUG: Attempting to read markdown file at absolute path: {:?}",
abs_path
);
match fs::read_to_string(&abs_path) {
Ok(md) => {
println!("DEBUG: Successfully read markdown file");
@ -170,9 +191,12 @@ impl ContractController {
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
context.insert("contract_section_content", &html_output);
},
}
Err(e) => {
let error_msg = format!("Error: Could not read contract section markdown at '{:?}': {}", abs_path, e);
let error_msg = format!(
"Error: Could not read contract section markdown at '{:?}': {}",
abs_path, e
);
println!("{}", error_msg);
context.insert("contract_section_content_error", &error_msg);
}
@ -181,11 +205,19 @@ impl ContractController {
}
// Count signed signers for the template
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count();
let signed_signers = contract
.signers
.iter()
.filter(|s| s.status == SignerStatus::Signed)
.count();
context.insert("signed_signers", &signed_signers);
// Count pending signers for the template
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
let pending_signers = contract
.signers
.iter()
.filter(|s| s.status == SignerStatus::Pending)
.count();
context.insert("pending_signers", &pending_signers);
// For demo purposes, set user_has_signed to false
@ -208,7 +240,7 @@ impl ContractController {
("Employment", "Employment Contract"),
("NDA", "Non-Disclosure Agreement"),
("SLA", "Service Level Agreement"),
("Other", "Other")
("Other", "Other"),
];
context.insert("contract_types", &contract_types);
@ -224,7 +256,9 @@ impl ContractController {
// In a real application, we would save the contract to the database
// For now, we'll just redirect to the contracts list
Ok(HttpResponse::Found().append_header(("Location", "/contracts")).finish())
Ok(HttpResponse::Found()
.append_header(("Location", "/contracts"))
.finish())
}
// Helper method to convert Contract to a JSON object for templates
@ -232,46 +266,99 @@ impl ContractController {
let mut map = serde_json::Map::new();
// Basic contract info
map.insert("id".to_string(), serde_json::Value::String(contract.id.clone()));
map.insert("title".to_string(), serde_json::Value::String(contract.title.clone()));
map.insert("description".to_string(), serde_json::Value::String(contract.description.clone()));
map.insert("status".to_string(), serde_json::Value::String(contract.status.as_str().to_string()));
map.insert("contract_type".to_string(), serde_json::Value::String(contract.contract_type.as_str().to_string()));
map.insert("created_by".to_string(), serde_json::Value::String(contract.created_by.clone()));
map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()));
map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()));
map.insert(
"id".to_string(),
serde_json::Value::String(contract.id.clone()),
);
map.insert(
"title".to_string(),
serde_json::Value::String(contract.title.clone()),
);
map.insert(
"description".to_string(),
serde_json::Value::String(contract.description.clone()),
);
map.insert(
"status".to_string(),
serde_json::Value::String(contract.status.as_str().to_string()),
);
map.insert(
"contract_type".to_string(),
serde_json::Value::String(contract.contract_type.as_str().to_string()),
);
map.insert(
"created_by".to_string(),
serde_json::Value::String(contract.created_by.clone()),
);
map.insert(
"created_at".to_string(),
serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()),
);
map.insert(
"updated_at".to_string(),
serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()),
);
// Organization info
if let Some(org) = &contract.organization_id {
map.insert("organization".to_string(), serde_json::Value::String(org.clone()));
map.insert(
"organization".to_string(),
serde_json::Value::String(org.clone()),
);
} else {
map.insert("organization".to_string(), serde_json::Value::Null);
}
// Add signers
let signers: Vec<serde_json::Value> = contract.signers.iter()
let signers: Vec<serde_json::Value> = contract
.signers
.iter()
.map(|s| {
let mut signer_map = serde_json::Map::new();
signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone()));
signer_map.insert("name".to_string(), serde_json::Value::String(s.name.clone()));
signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone()));
signer_map.insert("status".to_string(), serde_json::Value::String(s.status.as_str().to_string()));
signer_map.insert(
"name".to_string(),
serde_json::Value::String(s.name.clone()),
);
signer_map.insert(
"email".to_string(),
serde_json::Value::String(s.email.clone()),
);
signer_map.insert(
"status".to_string(),
serde_json::Value::String(s.status.as_str().to_string()),
);
if let Some(signed_at) = s.signed_at {
signer_map.insert("signed_at".to_string(), serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()));
signer_map.insert(
"signed_at".to_string(),
serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()),
);
} else {
// For display purposes, add a placeholder date for pending signers
if s.status == SignerStatus::Pending {
signer_map.insert("signed_at".to_string(), serde_json::Value::String("Pending".to_string()));
signer_map.insert(
"signed_at".to_string(),
serde_json::Value::String("Pending".to_string()),
);
} else if s.status == SignerStatus::Rejected {
signer_map.insert("signed_at".to_string(), serde_json::Value::String("Rejected".to_string()));
signer_map.insert(
"signed_at".to_string(),
serde_json::Value::String("Rejected".to_string()),
);
}
}
if let Some(comments) = &s.comments {
signer_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
signer_map.insert(
"comments".to_string(),
serde_json::Value::String(comments.clone()),
);
} else {
signer_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
signer_map.insert(
"comments".to_string(),
serde_json::Value::String("".to_string()),
);
}
serde_json::Value::Object(signer_map)
@ -281,91 +368,212 @@ impl ContractController {
map.insert("signers".to_string(), serde_json::Value::Array(signers));
// Add pending_signers count for templates
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers)));
let pending_signers = contract
.signers
.iter()
.filter(|s| s.status == SignerStatus::Pending)
.count();
map.insert(
"pending_signers".to_string(),
serde_json::Value::Number(serde_json::Number::from(pending_signers)),
);
// Add signed_signers count for templates
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count();
map.insert("signed_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(signed_signers)));
let signed_signers = contract
.signers
.iter()
.filter(|s| s.status == SignerStatus::Signed)
.count();
map.insert(
"signed_signers".to_string(),
serde_json::Value::Number(serde_json::Number::from(signed_signers)),
);
// Add revisions
let revisions: Vec<serde_json::Value> = contract.revisions.iter()
let revisions: Vec<serde_json::Value> = contract
.revisions
.iter()
.map(|r| {
let mut revision_map = serde_json::Map::new();
revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(r.version)));
revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone()));
revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()));
revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone()));
revision_map.insert(
"version".to_string(),
serde_json::Value::Number(serde_json::Number::from(r.version)),
);
revision_map.insert(
"content".to_string(),
serde_json::Value::String(r.content.clone()),
);
revision_map.insert(
"created_at".to_string(),
serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()),
);
revision_map.insert(
"created_by".to_string(),
serde_json::Value::String(r.created_by.clone()),
);
if let Some(comments) = &r.comments {
revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
revision_map.insert(
"comments".to_string(),
serde_json::Value::String(comments.clone()),
);
// Add notes field using comments since ContractRevision doesn't have a notes field
revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone()));
revision_map.insert(
"notes".to_string(),
serde_json::Value::String(comments.clone()),
);
} else {
revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string()));
revision_map.insert(
"comments".to_string(),
serde_json::Value::String("".to_string()),
);
revision_map.insert(
"notes".to_string(),
serde_json::Value::String("".to_string()),
);
}
serde_json::Value::Object(revision_map)
})
.collect();
map.insert("revisions".to_string(), serde_json::Value::Array(revisions.clone()));
map.insert(
"revisions".to_string(),
serde_json::Value::Array(revisions.clone()),
);
// Add current_version
map.insert("current_version".to_string(), serde_json::Value::Number(serde_json::Number::from(contract.current_version)));
map.insert(
"current_version".to_string(),
serde_json::Value::Number(serde_json::Number::from(contract.current_version)),
);
// Add latest_revision as an object
if !contract.revisions.is_empty() {
// Find the latest revision based on version number
if let Some(latest) = contract.revisions.iter().max_by_key(|r| r.version) {
let mut latest_revision_map = serde_json::Map::new();
latest_revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(latest.version)));
latest_revision_map.insert("content".to_string(), serde_json::Value::String(latest.content.clone()));
latest_revision_map.insert("created_at".to_string(), serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string()));
latest_revision_map.insert("created_by".to_string(), serde_json::Value::String(latest.created_by.clone()));
latest_revision_map.insert(
"version".to_string(),
serde_json::Value::Number(serde_json::Number::from(latest.version)),
);
latest_revision_map.insert(
"content".to_string(),
serde_json::Value::String(latest.content.clone()),
);
latest_revision_map.insert(
"created_at".to_string(),
serde_json::Value::String(latest.created_at.format("%Y-%m-%d").to_string()),
);
latest_revision_map.insert(
"created_by".to_string(),
serde_json::Value::String(latest.created_by.clone()),
);
if let Some(comments) = &latest.comments {
latest_revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
latest_revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone()));
latest_revision_map.insert(
"comments".to_string(),
serde_json::Value::String(comments.clone()),
);
latest_revision_map.insert(
"notes".to_string(),
serde_json::Value::String(comments.clone()),
);
} else {
latest_revision_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
latest_revision_map.insert("notes".to_string(), serde_json::Value::String("".to_string()));
latest_revision_map.insert(
"comments".to_string(),
serde_json::Value::String("".to_string()),
);
latest_revision_map.insert(
"notes".to_string(),
serde_json::Value::String("".to_string()),
);
}
map.insert("latest_revision".to_string(), serde_json::Value::Object(latest_revision_map));
map.insert(
"latest_revision".to_string(),
serde_json::Value::Object(latest_revision_map),
);
} else {
// Create an empty latest_revision object to avoid template errors
let mut empty_revision = serde_json::Map::new();
empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string()));
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string()));
empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string()));
empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string()));
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string()));
empty_revision.insert(
"version".to_string(),
serde_json::Value::Number(serde_json::Number::from(0)),
);
empty_revision.insert(
"content".to_string(),
serde_json::Value::String("No content available".to_string()),
);
empty_revision.insert(
"created_at".to_string(),
serde_json::Value::String("N/A".to_string()),
);
empty_revision.insert(
"created_by".to_string(),
serde_json::Value::String("N/A".to_string()),
);
empty_revision.insert(
"comments".to_string(),
serde_json::Value::String("".to_string()),
);
empty_revision.insert(
"notes".to_string(),
serde_json::Value::String("".to_string()),
);
map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision));
map.insert(
"latest_revision".to_string(),
serde_json::Value::Object(empty_revision),
);
}
} else {
// Create an empty latest_revision object to avoid template errors
let mut empty_revision = serde_json::Map::new();
empty_revision.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
empty_revision.insert("content".to_string(), serde_json::Value::String("No content available".to_string()));
empty_revision.insert("created_at".to_string(), serde_json::Value::String("N/A".to_string()));
empty_revision.insert("created_by".to_string(), serde_json::Value::String("N/A".to_string()));
empty_revision.insert("comments".to_string(), serde_json::Value::String("".to_string()));
empty_revision.insert("notes".to_string(), serde_json::Value::String("".to_string()));
empty_revision.insert(
"version".to_string(),
serde_json::Value::Number(serde_json::Number::from(0)),
);
empty_revision.insert(
"content".to_string(),
serde_json::Value::String("No content available".to_string()),
);
empty_revision.insert(
"created_at".to_string(),
serde_json::Value::String("N/A".to_string()),
);
empty_revision.insert(
"created_by".to_string(),
serde_json::Value::String("N/A".to_string()),
);
empty_revision.insert(
"comments".to_string(),
serde_json::Value::String("".to_string()),
);
empty_revision.insert(
"notes".to_string(),
serde_json::Value::String("".to_string()),
);
map.insert("latest_revision".to_string(), serde_json::Value::Object(empty_revision));
map.insert(
"latest_revision".to_string(),
serde_json::Value::Object(empty_revision),
);
}
// Add effective and expiration dates if present
if let Some(effective_date) = &contract.effective_date {
map.insert("effective_date".to_string(), serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()));
map.insert(
"effective_date".to_string(),
serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()),
);
}
if let Some(expiration_date) = &contract.expiration_date {
map.insert("expiration_date".to_string(), serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()));
map.insert(
"expiration_date".to_string(),
serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()),
);
}
map
@ -554,7 +762,6 @@ impl ContractController {
]),
};
// Add potential signers to contract 3 (still in draft)
contract3.signers.push(ContractSigner {
id: "signer-006".to_string(),
@ -576,8 +783,7 @@ impl ContractController {
// Add ToC and content directory to contract 3
contract3.content_dir = Some("src/content/contract-003".to_string());
contract3.toc = Some(vec![
TocItem {
contract3.toc = Some(vec![TocItem {
title: "Digital Asset Tokenization Agreement".to_string(),
file: "cover.md".to_string(),
children: vec![
@ -622,8 +828,7 @@ impl ContractController {
children: vec![],
},
],
}
]);
}]);
// No revision content for contract 3, content is in markdown files.
// Mock contract 4 - Rejected

View File

@ -1,12 +1,15 @@
use actix_web::{web, HttpResponse, Result};
use actix_web::HttpRequest;
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use actix_web::{HttpResponse, Result, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use tera::{Context, Tera};
use uuid::Uuid;
use crate::models::asset::{Asset, AssetType, AssetStatus};
use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB};
use crate::models::asset::Asset;
use crate::models::defi::{
DEFI_DB, DefiPosition, DefiPositionStatus, DefiPositionType, ProvidingPosition,
ReceivingPosition,
};
use crate::utils::render_template;
// Form structs for DeFi operations
@ -26,6 +29,7 @@ pub struct ReceivingForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LiquidityForm {
pub first_token: String,
pub first_amount: f64,
@ -35,6 +39,7 @@ pub struct LiquidityForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StakingForm {
pub asset_id: String,
pub amount: f64,
@ -49,6 +54,7 @@ pub struct SwapForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct CollateralForm {
pub asset_id: String,
pub amount: f64,
@ -116,7 +122,10 @@ impl DefiController {
}
// Process providing request
pub async fn create_providing(_tmpl: web::Data<Tera>, form: web::Form<ProvidingForm>) -> Result<HttpResponse> {
pub async fn create_providing(
_tmpl: web::Data<Tera>,
form: web::Form<ProvidingForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing providing request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database)
@ -134,7 +143,8 @@ impl DefiController {
_ => 4.2, // Default to 30 days rate
};
let return_amount = form.amount + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
let return_amount = form.amount
+ (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
// Create a new providing position
let providing_position = ProvidingPosition {
@ -164,9 +174,15 @@ impl DefiController {
}
// Redirect with success message
let success_message = format!("Successfully provided {} {} for {} days", form.amount, asset.name, form.duration);
let success_message = format!(
"Successfully provided {} {} for {} days",
form.amount, asset.name, form.duration
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
} else {
// Asset not found, redirect with error
@ -177,7 +193,10 @@ impl DefiController {
}
// Process receiving request
pub async fn create_receiving(_tmpl: web::Data<Tera>, form: web::Form<ReceivingForm>) -> Result<HttpResponse> {
pub async fn create_receiving(
_tmpl: web::Data<Tera>,
form: web::Form<ReceivingForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing receiving request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database)
@ -196,11 +215,13 @@ impl DefiController {
};
// Calculate profit share and total to repay
let profit_share = form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
let profit_share =
form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
let total_to_repay = form.amount + profit_share;
// Calculate collateral value and ratio
let collateral_value = form.collateral_amount * collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
let collateral_value = form.collateral_amount
* collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
let collateral_ratio = (collateral_value / form.amount) * 100.0;
// Create a new receiving position
@ -238,10 +259,15 @@ impl DefiController {
}
// Redirect with success message
let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral",
form.amount, form.collateral_amount, collateral_asset.name);
let success_message = format!(
"Successfully borrowed {} ZDFZ using {} {} as collateral",
form.amount, form.collateral_amount, collateral_asset.name
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
} else {
// Asset not found, redirect with error
@ -252,22 +278,33 @@ impl DefiController {
}
// Process liquidity provision
pub async fn add_liquidity(_tmpl: web::Data<Tera>, form: web::Form<LiquidityForm>) -> Result<HttpResponse> {
pub async fn add_liquidity(
_tmpl: web::Data<Tera>,
form: web::Form<LiquidityForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing liquidity provision: {:?}", form);
// In a real application, this would add liquidity to a pool in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully added liquidity: {} {} and {} {}",
form.first_amount, form.first_token, form.second_amount, form.second_token);
let success_message = format!(
"Successfully added liquidity: {} {} and {} {}",
form.first_amount, form.first_token, form.second_amount, form.second_token
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
}
// Process staking request
pub async fn create_staking(_tmpl: web::Data<Tera>, form: web::Form<StakingForm>) -> Result<HttpResponse> {
pub async fn create_staking(
_tmpl: web::Data<Tera>,
form: web::Form<StakingForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing staking request: {:?}", form);
// In a real application, this would create a staking position in the database
@ -276,27 +313,41 @@ impl DefiController {
let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
}
// Process token swap
pub async fn swap_tokens(_tmpl: web::Data<Tera>, form: web::Form<SwapForm>) -> Result<HttpResponse> {
pub async fn swap_tokens(
_tmpl: web::Data<Tera>,
form: web::Form<SwapForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing token swap: {:?}", form);
// In a real application, this would perform a token swap in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully swapped {} {} to {}",
form.from_amount, form.from_token, form.to_token);
let success_message = format!(
"Successfully swapped {} {} to {}",
form.from_amount, form.from_token, form.to_token
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
}
// Process collateral position creation
pub async fn create_collateral(_tmpl: web::Data<Tera>, form: web::Form<CollateralForm>) -> Result<HttpResponse> {
pub async fn create_collateral(
_tmpl: web::Data<Tera>,
form: web::Form<CollateralForm>,
) -> Result<HttpResponse> {
println!("DEBUG: Processing collateral creation: {:?}", form);
// In a real application, this would create a collateral position in the database
@ -309,11 +360,16 @@ impl DefiController {
_ => "collateralization",
};
let success_message = format!("Successfully collateralized {} {} for {}",
form.amount, form.asset_id, purpose_str);
let success_message = format!(
"Successfully collateralized {} {} for {}",
form.amount, form.asset_id, purpose_str
);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.append_header((
"Location",
format!("/defi?success={}", urlencoding::encode(&success_message)),
))
.finish())
}
@ -322,12 +378,32 @@ impl DefiController {
let mut stats = serde_json::Map::new();
// Handle Option<Number> by unwrapping with expect
stats.insert("total_value_locked".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(1250000.0).expect("Valid float")));
stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")));
stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")));
stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12)));
stats.insert("active_stakers".to_string(), serde_json::Value::Number(serde_json::Number::from(156)));
stats.insert("total_swap_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")));
stats.insert(
"total_value_locked".to_string(),
serde_json::Value::Number(
serde_json::Number::from_f64(1250000.0).expect("Valid float"),
),
);
stats.insert(
"providing_volume".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")),
);
stats.insert(
"receiving_volume".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")),
);
stats.insert(
"liquidity_pools_count".to_string(),
serde_json::Value::Number(serde_json::Number::from(12)),
);
stats.insert(
"active_stakers".to_string(),
serde_json::Value::Number(serde_json::Number::from(156)),
);
stats.insert(
"total_swap_volume".to_string(),
serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")),
);
stats
}
@ -336,25 +412,61 @@ impl DefiController {
fn asset_to_json(asset: &Asset) -> serde_json::Map<String, serde_json::Value> {
let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::String(asset.id.clone()));
map.insert("name".to_string(), serde_json::Value::String(asset.name.clone()));
map.insert("description".to_string(), serde_json::Value::String(asset.description.clone()));
map.insert("asset_type".to_string(), serde_json::Value::String(asset.asset_type.as_str().to_string()));
map.insert("status".to_string(), serde_json::Value::String(asset.status.as_str().to_string()));
map.insert(
"id".to_string(),
serde_json::Value::String(asset.id.clone()),
);
map.insert(
"name".to_string(),
serde_json::Value::String(asset.name.clone()),
);
map.insert(
"description".to_string(),
serde_json::Value::String(asset.description.clone()),
);
map.insert(
"asset_type".to_string(),
serde_json::Value::String(asset.asset_type.as_str().to_string()),
);
map.insert(
"status".to_string(),
serde_json::Value::String(asset.status.as_str().to_string()),
);
// Add current valuation
if let Some(latest) = asset.latest_valuation() {
if let Some(num) = serde_json::Number::from_f64(latest.value) {
map.insert("current_valuation".to_string(), serde_json::Value::Number(num));
map.insert(
"current_valuation".to_string(),
serde_json::Value::Number(num),
);
} else {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
map.insert(
"current_valuation".to_string(),
serde_json::Value::Number(serde_json::Number::from(0)),
);
}
map.insert("valuation_currency".to_string(), serde_json::Value::String(latest.currency.clone()));
map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()));
map.insert(
"valuation_currency".to_string(),
serde_json::Value::String(latest.currency.clone()),
);
map.insert(
"valuation_date".to_string(),
serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()),
);
} else {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string()));
map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".to_string()));
map.insert(
"current_valuation".to_string(),
serde_json::Value::Number(serde_json::Number::from(0)),
);
map.insert(
"valuation_currency".to_string(),
serde_json::Value::String("USD".to_string()),
);
map.insert(
"valuation_date".to_string(),
serde_json::Value::String("N/A".to_string()),
);
}
map

View File

@ -609,6 +609,7 @@ impl FlowController {
/// Form for creating a new flow
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct FlowForm {
/// Flow name
pub name: String,
@ -620,6 +621,7 @@ pub struct FlowForm {
/// Form for marking a step as stuck
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct StuckForm {
/// Reason for being stuck
pub reason: String,
@ -627,6 +629,7 @@ pub struct StuckForm {
/// Form for adding a log to a step
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LogForm {
/// Log message
pub message: String,

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,7 @@ impl HomeController {
/// Represents the data submitted in the contact form
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
pub struct ContactForm {
pub name: String,
pub email: String,

View File

@ -1,12 +1,11 @@
use actix_web::{web, HttpResponse, Result, http};
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use actix_web::{HttpResponse, Result, http, web};
use chrono::{Duration, Utc};
use serde::Deserialize;
use uuid::Uuid;
use tera::{Context, Tera};
use crate::models::asset::{Asset, AssetType, AssetStatus};
use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
use crate::controllers::asset::AssetController;
use crate::models::asset::{Asset, AssetStatus, AssetType};
use crate::models::marketplace::{Listing, ListingStatus, ListingType, MarketplaceStatistics};
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
@ -22,6 +21,7 @@ pub struct ListingForm {
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct BidForm {
pub amount: f64,
pub currency: String,
@ -43,13 +43,15 @@ impl MarketplaceController {
let stats = MarketplaceStatistics::new(&listings);
// Get featured listings (up to 4)
let featured_listings: Vec<&Listing> = listings.iter()
let featured_listings: Vec<&Listing> = listings
.iter()
.filter(|l| l.featured && l.status == ListingStatus::Active)
.take(4)
.collect();
// Get recent listings (up to 8)
let mut recent_listings: Vec<&Listing> = listings.iter()
let mut recent_listings: Vec<&Listing> = listings
.iter()
.filter(|l| l.status == ListingStatus::Active)
.collect();
@ -58,7 +60,8 @@ impl MarketplaceController {
let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
// Get recent sales (up to 5)
let mut recent_sales: Vec<&Listing> = listings.iter()
let mut recent_sales: Vec<&Listing> = listings
.iter()
.filter(|l| l.status == ListingStatus::Sold)
.collect();
@ -87,18 +90,24 @@ impl MarketplaceController {
let listings = Self::get_mock_listings();
// Filter active listings
let active_listings: Vec<&Listing> = listings.iter()
let active_listings: Vec<&Listing> = listings
.iter()
.filter(|l| l.status == ListingStatus::Active)
.collect();
context.insert("active_page", &"marketplace");
context.insert("listings", &active_listings);
context.insert("listing_types", &[
context.insert(
"listing_types",
&[
ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(),
ListingType::Exchange.as_str(),
]);
context.insert("asset_types", &[
],
);
context.insert(
"asset_types",
&[
AssetType::Token.as_str(),
AssetType::Artwork.as_str(),
AssetType::RealEstate.as_str(),
@ -107,7 +116,8 @@ impl MarketplaceController {
AssetType::Share.as_str(),
AssetType::Bond.as_str(),
AssetType::Other.as_str(),
]);
],
);
render_template(&tmpl, "marketplace/listings.html", &context)
}
@ -120,9 +130,8 @@ impl MarketplaceController {
// Filter by current user (mock user ID)
let user_id = "user-123";
let my_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.seller_id == user_id)
.collect();
let my_listings: Vec<&Listing> =
listings.iter().filter(|l| l.seller_id == user_id).collect();
context.insert("active_page", &"marketplace");
context.insert("listings", &my_listings);
@ -131,7 +140,10 @@ impl MarketplaceController {
}
// Display listing details
pub async fn listing_detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
pub async fn listing_detail(
tmpl: web::Data<Tera>,
path: web::Path<String>,
) -> Result<HttpResponse> {
let listing_id = path.into_inner();
let mut context = Context::new();
@ -142,15 +154,19 @@ impl MarketplaceController {
if let Some(listing) = listing {
// Get similar listings (same asset type, active)
let similar_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.asset_type == listing.asset_type &&
l.status == ListingStatus::Active &&
l.id != listing.id)
let similar_listings: Vec<&Listing> = listings
.iter()
.filter(|l| {
l.asset_type == listing.asset_type
&& l.status == ListingStatus::Active
&& l.id != listing.id
})
.take(4)
.collect();
// Get highest bid amount and minimum bid for auction listings
let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction {
let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction
{
if let Some(bid) = listing.highest_bid() {
(Some(bid.amount), bid.amount + 1.0)
} else {
@ -186,17 +202,21 @@ impl MarketplaceController {
let assets = AssetController::get_mock_assets();
let user_id = "user-123"; // Mock user ID
let user_assets: Vec<&Asset> = assets.iter()
let user_assets: Vec<&Asset> = assets
.iter()
.filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
.collect();
context.insert("active_page", &"marketplace");
context.insert("assets", &user_assets);
context.insert("listing_types", &[
context.insert(
"listing_types",
&[
ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(),
ListingType::Exchange.as_str(),
]);
],
);
render_template(&tmpl, "marketplace/create_listing.html", &context)
}
@ -215,7 +235,8 @@ impl MarketplaceController {
if let Some(asset) = asset {
// Process tags
let tags = match form.tags {
Some(tags_str) => tags_str.split(',')
Some(tags_str) => tags_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
@ -223,9 +244,9 @@ impl MarketplaceController {
};
// Calculate expiration date if provided
let expires_at = form.duration_days.map(|days| {
Utc::now() + Duration::days(days as i64)
});
let expires_at = form
.duration_days
.map(|days| Utc::now() + Duration::days(days as i64));
// Parse listing type
let listing_type = match form.listing_type.as_str() {
@ -273,13 +294,14 @@ impl MarketplaceController {
}
// Submit a bid on an auction listing
#[allow(dead_code)]
pub async fn submit_bid(
tmpl: web::Data<Tera>,
_tmpl: web::Data<Tera>,
path: web::Path<String>,
form: web::Form<BidForm>,
_form: web::Form<BidForm>,
) -> Result<HttpResponse> {
let listing_id = path.into_inner();
let form = form.into_inner();
let _form = _form.into_inner();
// In a real application, we would:
// 1. Find the listing in the database
@ -289,13 +311,16 @@ impl MarketplaceController {
// For now, we'll just redirect back to the listing
Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
.insert_header((
http::header::LOCATION,
format!("/marketplace/{}", listing_id),
))
.finish())
}
// Purchase a fixed-price listing
pub async fn purchase_listing(
tmpl: web::Data<Tera>,
_tmpl: web::Data<Tera>,
path: web::Path<String>,
form: web::Form<PurchaseForm>,
) -> Result<HttpResponse> {
@ -305,7 +330,10 @@ impl MarketplaceController {
if !form.agree_to_terms {
// User must agree to terms
return Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
.insert_header((
http::header::LOCATION,
format!("/marketplace/{}", listing_id),
))
.finish());
}
@ -324,7 +352,7 @@ impl MarketplaceController {
// Cancel a listing
pub async fn cancel_listing(
tmpl: web::Data<Tera>,
_tmpl: web::Data<Tera>,
path: web::Path<String>,
) -> Result<HttpResponse> {
let _listing_id = path.into_inner();
@ -368,7 +396,10 @@ impl MarketplaceController {
let mut listing = Listing::new(
format!("{} for Sale", asset.name),
format!("This is a great opportunity to own {}. {}", asset.name, asset.description),
format!(
"This is a great opportunity to own {}. {}",
asset.name, asset.description
),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
@ -427,7 +458,8 @@ impl MarketplaceController {
let num_bids = 2 + (i % 3);
for j in 0..num_bids {
let bidder_index = (j + 1) % user_ids.len();
if bidder_index != user_index { // Ensure seller isn't bidding
if bidder_index != user_index {
// Ensure seller isn't bidding
let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
let _ = listing.add_bid(
user_ids[bidder_index].to_string(),
@ -465,7 +497,10 @@ impl MarketplaceController {
let listing = Listing::new(
format!("Trade: {}", asset.name),
format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name),
format!(
"Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.",
asset.name
),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),

View File

@ -0,0 +1,360 @@
use chrono::{DateTime, Utc};
use heromodels::{
db::{Collection, Db},
models::calendar::{AttendanceStatus, Attendee, Calendar, Event},
};
use super::db::get_db;
/// Creates a new calendar and saves it to the database. Returns the saved calendar and its ID.
pub fn create_new_calendar(
name: &str,
description: Option<&str>,
owner_id: Option<u32>,
is_public: bool,
color: Option<&str>,
) -> Result<(u32, Calendar), String> {
let db = get_db().expect("Can get DB");
// Create a new calendar (with auto-generated ID)
let mut calendar = Calendar::new(None, name);
if let Some(desc) = description {
calendar = calendar.description(desc);
}
if let Some(owner) = owner_id {
calendar = calendar.owner_id(owner);
}
if let Some(col) = color {
calendar = calendar.color(col);
}
calendar = calendar.is_public(is_public);
// Save the calendar to the database
let collection = db
.collection::<Calendar>()
.expect("can open calendar collection");
let (calendar_id, saved_calendar) = collection.set(&calendar).expect("can save calendar");
Ok((calendar_id, saved_calendar))
}
/// Creates a new event and saves it to the database. Returns the saved event and its ID.
pub fn create_new_event(
title: &str,
description: Option<&str>,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
location: Option<&str>,
color: Option<&str>,
all_day: bool,
created_by: Option<u32>,
category: Option<&str>,
reminder_minutes: Option<i32>,
) -> Result<(u32, Event), String> {
let db = get_db().expect("Can get DB");
// Create a new event (with auto-generated ID)
let mut event = Event::new(title, start_time, end_time);
if let Some(desc) = description {
event = event.description(desc);
}
if let Some(loc) = location {
event = event.location(loc);
}
if let Some(col) = color {
event = event.color(col);
}
if let Some(user_id) = created_by {
event = event.created_by(user_id);
}
if let Some(cat) = category {
event = event.category(cat);
}
if let Some(reminder) = reminder_minutes {
event = event.reminder_minutes(reminder);
}
event = event.all_day(all_day);
// Save the event to the database
let collection = db.collection::<Event>().expect("can open event collection");
let (event_id, saved_event) = collection.set(&event).expect("can save event");
Ok((event_id, saved_event))
}
/// Loads all calendars from the database and returns them as a Vec<Calendar>.
pub fn get_calendars() -> Result<Vec<Calendar>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.expect("can open calendar collection");
// Try to load all calendars, but handle deserialization errors gracefully
let calendars = match collection.get_all() {
Ok(calendars) => calendars,
Err(e) => {
eprintln!("Error loading calendars: {:?}", e);
vec![] // Return an empty vector if there's an error
}
};
Ok(calendars)
}
/// Loads all events from the database and returns them as a Vec<Event>.
pub fn get_events() -> Result<Vec<Event>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db.collection::<Event>().expect("can open event collection");
// Try to load all events, but handle deserialization errors gracefully
let events = match collection.get_all() {
Ok(events) => events,
Err(e) => {
eprintln!("Error loading events: {:?}", e);
vec![] // Return an empty vector if there's an error
}
};
Ok(events)
}
/// Fetches a single calendar by its ID from the database.
pub fn get_calendar_by_id(calendar_id: u32) -> Result<Option<Calendar>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.map_err(|e| format!("Collection error: {:?}", e))?;
match collection.get_by_id(calendar_id) {
Ok(calendar) => Ok(calendar),
Err(e) => {
eprintln!("Error fetching calendar by id {}: {:?}", calendar_id, e);
Err(format!("Failed to fetch calendar: {:?}", e))
}
}
}
/// Fetches a single event by its ID from the database.
pub fn get_event_by_id(event_id: u32) -> Result<Option<Event>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Event>()
.map_err(|e| format!("Collection error: {:?}", e))?;
match collection.get_by_id(event_id) {
Ok(event) => Ok(event),
Err(e) => {
eprintln!("Error fetching event by id {}: {:?}", event_id, e);
Err(format!("Failed to fetch event: {:?}", e))
}
}
}
/// Creates a new attendee and saves it to the database. Returns the saved attendee and its ID.
pub fn create_new_attendee(
contact_id: u32,
status: AttendanceStatus,
) -> Result<(u32, Attendee), String> {
let db = get_db().expect("Can get DB");
// Create a new attendee (with auto-generated ID)
let attendee = Attendee::new(contact_id).status(status);
// Save the attendee to the database
let collection = db
.collection::<Attendee>()
.expect("can open attendee collection");
let (attendee_id, saved_attendee) = collection.set(&attendee).expect("can save attendee");
Ok((attendee_id, saved_attendee))
}
/// Fetches a single attendee by its ID from the database.
pub fn get_attendee_by_id(attendee_id: u32) -> Result<Option<Attendee>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Attendee>()
.map_err(|e| format!("Collection error: {:?}", e))?;
match collection.get_by_id(attendee_id) {
Ok(attendee) => Ok(attendee),
Err(e) => {
eprintln!("Error fetching attendee by id {}: {:?}", attendee_id, e);
Err(format!("Failed to fetch attendee: {:?}", e))
}
}
}
/// Updates attendee status in the database and returns the updated attendee.
pub fn update_attendee_status(
attendee_id: u32,
status: AttendanceStatus,
) -> Result<Attendee, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Attendee>()
.map_err(|e| format!("Collection error: {:?}", e))?;
if let Some(mut attendee) = collection
.get_by_id(attendee_id)
.map_err(|e| format!("Failed to fetch attendee: {:?}", e))?
{
attendee = attendee.status(status);
let (_, updated_attendee) = collection
.set(&attendee)
.map_err(|e| format!("Failed to update attendee: {:?}", e))?;
Ok(updated_attendee)
} else {
Err("Attendee not found".to_string())
}
}
/// Add attendee to event
pub fn add_attendee_to_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Event>()
.map_err(|e| format!("Collection error: {:?}", e))?;
if let Some(mut event) = collection
.get_by_id(event_id)
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
{
event = event.add_attendee(attendee_id);
let (_, updated_event) = collection
.set(&event)
.map_err(|e| format!("Failed to update event: {:?}", e))?;
Ok(updated_event)
} else {
Err("Event not found".to_string())
}
}
/// Remove attendee from event
pub fn remove_attendee_from_event(event_id: u32, attendee_id: u32) -> Result<Event, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Event>()
.map_err(|e| format!("Collection error: {:?}", e))?;
if let Some(mut event) = collection
.get_by_id(event_id)
.map_err(|e| format!("Failed to fetch event: {:?}", e))?
{
event = event.remove_attendee(attendee_id);
let (_, updated_event) = collection
.set(&event)
.map_err(|e| format!("Failed to update event: {:?}", e))?;
Ok(updated_event)
} else {
Err("Event not found".to_string())
}
}
/// Add event to calendar
pub fn add_event_to_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.map_err(|e| format!("Collection error: {:?}", e))?;
if let Some(mut calendar) = collection
.get_by_id(calendar_id)
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
{
calendar = calendar.add_event(event_id as i64);
let (_, updated_calendar) = collection
.set(&calendar)
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
Ok(updated_calendar)
} else {
Err("Calendar not found".to_string())
}
}
/// Remove event from calendar
pub fn remove_event_from_calendar(calendar_id: u32, event_id: u32) -> Result<Calendar, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.map_err(|e| format!("Collection error: {:?}", e))?;
if let Some(mut calendar) = collection
.get_by_id(calendar_id)
.map_err(|e| format!("Failed to fetch calendar: {:?}", e))?
{
calendar = calendar.remove_event(event_id as i64);
let (_, updated_calendar) = collection
.set(&calendar)
.map_err(|e| format!("Failed to update calendar: {:?}", e))?;
Ok(updated_calendar)
} else {
Err("Calendar not found".to_string())
}
}
/// Deletes a calendar from the database.
pub fn delete_calendar(calendar_id: u32) -> Result<(), String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.map_err(|e| format!("Collection error: {:?}", e))?;
collection
.delete_by_id(calendar_id)
.map_err(|e| format!("Failed to delete calendar: {:?}", e))?;
Ok(())
}
/// Deletes an event from the database.
pub fn delete_event(event_id: u32) -> Result<(), String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Event>()
.map_err(|e| format!("Collection error: {:?}", e))?;
collection
.delete_by_id(event_id)
.map_err(|e| format!("Failed to delete event: {:?}", e))?;
Ok(())
}
/// Gets or creates a calendar for a user. If the user already has a calendar, returns it.
/// If not, creates a new calendar for the user and returns it.
pub fn get_or_create_user_calendar(user_id: u32, user_name: &str) -> Result<Calendar, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Calendar>()
.map_err(|e| format!("Collection error: {:?}", e))?;
// Try to find existing calendar for this user
let calendars = match collection.get_all() {
Ok(calendars) => calendars,
Err(e) => {
eprintln!("Error loading calendars: {:?}", e);
vec![] // Return an empty vector if there's an error
}
};
// Look for a calendar owned by this user
for calendar in calendars {
if let Some(owner_id) = calendar.owner_id {
if owner_id == user_id {
return Ok(calendar);
}
}
}
// No calendar found for this user, create a new one
let calendar_name = format!("{}'s Calendar", user_name);
let (_, new_calendar) = create_new_calendar(
&calendar_name,
Some("Personal calendar"),
Some(user_id),
false, // Private calendar
Some("#4285F4"), // Default blue color
)?;
Ok(new_calendar)
}

View File

@ -0,0 +1,17 @@
use std::path::PathBuf;
use heromodels::db::hero::OurDB;
/// The path to the database file. Change this as needed for your environment.
pub const DB_PATH: &str = "/tmp/freezone_db";
/// Returns a shared OurDB instance for the given path. You can wrap this in Arc/Mutex for concurrent access if needed.
pub fn get_db() -> Result<OurDB, String> {
let db_path = PathBuf::from(DB_PATH);
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
// Temporarily reset the database to fix the serialization issue
let db = heromodels::db::hero::OurDB::new(db_path, false).expect("Can create DB");
Ok(db)
}

View File

@ -0,0 +1,257 @@
use chrono::{Duration, Utc};
use heromodels::{
db::{Collection, Db},
models::governance::{Activity, ActivityType, Proposal, ProposalStatus},
};
use super::db::get_db;
/// Creates a new proposal and saves it to the database. Returns the saved proposal and its ID.
pub fn create_new_proposal(
creator_id: &str,
creator_name: &str,
title: &str,
description: &str,
status: ProposalStatus,
voting_start_date: Option<chrono::DateTime<Utc>>,
voting_end_date: Option<chrono::DateTime<Utc>>,
) -> Result<(u32, Proposal), String> {
let db = get_db().expect("Can get DB");
let created_at = Utc::now();
let updated_at = created_at;
// Create a new proposal (with auto-generated ID)
let proposal = Proposal::new(
None,
creator_id,
creator_name,
title,
description,
status,
created_at,
updated_at,
voting_start_date.unwrap_or_else(Utc::now),
voting_end_date.unwrap_or_else(|| Utc::now() + Duration::days(7)),
);
// Save the proposal to the database
let collection = db
.collection::<Proposal>()
.expect("can open proposal collection");
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
Ok((proposal_id, saved_proposal))
}
/// Loads all proposals from the database and returns them as a Vec<Proposal>.
pub fn get_proposals() -> Result<Vec<Proposal>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Proposal>()
.expect("can open proposal collection");
// Try to load all proposals, but handle deserialization errors gracefully
let proposals = match collection.get_all() {
Ok(props) => props,
Err(e) => {
eprintln!("Error loading proposals: {:?}", e);
vec![] // Return an empty vector if there's an error
}
};
Ok(proposals)
}
/// Fetches a single proposal by its ID from the database.
pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String> {
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Proposal>()
.map_err(|e| format!("Collection error: {:?}", e))?;
match collection.get_by_id(proposal_id) {
Ok(proposal) => Ok(Some(proposal.expect("proposal not found"))),
Err(e) => {
eprintln!("Error fetching proposal by id {}: {:?}", proposal_id, e);
Err(format!("Failed to fetch proposal: {:?}", e))
}
}
}
/// Submits a vote on a proposal and returns the updated proposal
pub fn submit_vote_on_proposal(
proposal_id: u32,
user_id: i32,
vote_type: &str,
shares_count: u32, // Default to 1 if not specified
comment: Option<String>,
) -> Result<Proposal, String> {
// Get the proposal from the database
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
let collection = db
.collection::<Proposal>()
.map_err(|e| format!("Collection error: {:?}", e))?;
// Get the proposal
let mut proposal = collection
.get_by_id(proposal_id)
.map_err(|e| format!("Failed to fetch proposal: {:?}", e))?
.ok_or_else(|| format!("Proposal not found with ID: {}", proposal_id))?;
// Ensure the proposal has vote options
// Check if the proposal already has options
if proposal.options.is_empty() {
// Add standard vote options if they don't exist
proposal = proposal.add_option(1, "Approve", Some("Approve the proposal"));
proposal = proposal.add_option(2, "Reject", Some("Reject the proposal"));
proposal = proposal.add_option(3, "Abstain", Some("Abstain from voting"));
}
// Map vote_type to option_id
let option_id = match vote_type {
"Yes" => 1, // Approve
"No" => 2, // Reject
"Abstain" => 3, // Abstain
_ => return Err(format!("Invalid vote type: {}", vote_type)),
};
// Since we're having issues with the cast_vote method, let's implement a workaround
// that directly updates the vote count for the selected option
// Check if the proposal is active
if proposal.status != ProposalStatus::Active {
return Err(format!(
"Cannot vote on a proposal with status: {:?}",
proposal.status
));
}
// Check if voting period is valid
let now = Utc::now();
if now > proposal.vote_end_date {
return Err("Voting period has ended".to_string());
}
if now < proposal.vote_start_date {
return Err("Voting period has not started yet".to_string());
}
// Find the option and increment its count
let mut option_found = false;
for option in &mut proposal.options {
if option.id == option_id {
option.count += shares_count as i64;
option_found = true;
break;
}
}
if !option_found {
return Err(format!("Option with ID {} not found", option_id));
}
// Record the vote in the proposal's ballots
// We'll create a simple ballot with an auto-generated ID
let ballot_id = proposal.ballots.len() as u32 + 1;
// Create a new ballot and add it to the proposal's ballots
use heromodels::models::governance::Ballot;
// Use the Ballot::new constructor which handles the BaseModelData creation
let mut ballot = Ballot::new(
Some(ballot_id),
user_id as u32,
option_id,
shares_count as i64,
);
// Set the comment if provided
ballot.comment = comment;
// Store the local time (EEST = UTC+3) as the vote timestamp
// This ensures the displayed time matches the user's local time
let utc_now = Utc::now();
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
let local_time = utc_now.with_timezone(&local_offset);
// Store the local time as a timestamp (this is what will be displayed)
ballot.base_data.created_at = local_time.timestamp();
// Add the ballot to the proposal's ballots
proposal.ballots.push(ballot);
// Update the proposal's updated_at timestamp
proposal.updated_at = Utc::now();
// Save the updated proposal
let (_, updated_proposal) = collection
.set(&proposal)
.map_err(|e| format!("Failed to save vote: {:?}", e))?;
Ok(updated_proposal)
}
#[allow(unused_assignments)]
/// Creates a new governance activity and saves it to the database using OurDB
pub fn create_activity(
proposal_id: u32,
proposal_title: &str,
creator_name: &str,
activity_type: &ActivityType,
) -> Result<(u32, Activity), String> {
let db = get_db().expect("Can get DB");
let mut activity = Activity::default();
match activity_type {
ActivityType::ProposalCreated => {
activity = Activity::proposal_created(proposal_id, proposal_title, creator_name);
}
ActivityType::VoteCast => {
activity = Activity::vote_cast(proposal_id, proposal_title, creator_name);
}
ActivityType::VotingStarted => {
activity = Activity::voting_started(proposal_id, proposal_title);
}
ActivityType::VotingEnded => {
activity = Activity::voting_ended(proposal_id, proposal_title);
}
}
// Save the proposal to the database
let collection = db
.collection::<Activity>()
.expect("can open activity collection");
let (proposal_id, saved_proposal) = collection.set(&activity).expect("can save proposal");
Ok((proposal_id, saved_proposal))
}
pub fn get_recent_activities() -> Result<Vec<Activity>, String> {
let db = get_db().expect("Can get DB");
let collection = db
.collection::<Activity>()
.map_err(|e| format!("Collection error: {:?}", e))?;
let mut db_activities = collection
.get_all()
.map_err(|e| format!("DB fetch error: {:?}", e))?;
// Sort by created_at descending
db_activities.sort_by(|a, b| b.created_at.cmp(&a.created_at));
// Take the top 10 most recent
let recent_activities = db_activities.into_iter().take(10).collect();
Ok(recent_activities)
}
pub fn get_all_activities() -> Result<Vec<Activity>, String> {
let db = get_db().expect("Can get DB");
let collection = db
.collection::<Activity>()
.map_err(|e| format!("Collection error: {:?}", e))?;
let db_activities = collection
.get_all()
.map_err(|e| format!("DB fetch error: {:?}", e))?;
Ok(db_activities)
}

View File

@ -0,0 +1,4 @@
pub mod calendar;
pub mod contracts;
pub mod db;
pub mod governance;

View File

@ -1,22 +1,23 @@
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;
mod db;
mod middleware;
mod models;
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;
@ -65,7 +66,8 @@ 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");
@ -77,6 +79,9 @@ async fn main() -> io::Result<()> {
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

View File

@ -112,6 +112,7 @@ pub struct Asset {
pub external_url: Option<String>,
}
#[allow(dead_code)]
impl Asset {
/// Creates a new asset
pub fn new(

View File

@ -1,61 +1,4 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Represents a calendar event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
/// Unique identifier for the event
pub id: String,
/// Title of the event
pub title: String,
/// Description of the event
pub description: String,
/// Start time of the event
pub start_time: DateTime<Utc>,
/// End time of the event
pub end_time: DateTime<Utc>,
/// Color of the event (hex code)
pub color: String,
/// Whether the event is an all-day event
pub all_day: bool,
/// User ID of the event creator
pub user_id: Option<String>,
}
impl CalendarEvent {
/// Creates a new calendar event
pub fn new(
title: String,
description: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
color: Option<String>,
all_day: bool,
user_id: Option<String>,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
title,
description,
start_time,
end_time,
color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue
all_day,
user_id,
}
}
/// Converts the event to a JSON string
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
/// Creates an event from a JSON string
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
// No imports needed for this module currently
/// Represents a view mode for the calendar
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -85,6 +85,7 @@ pub struct ContractSigner {
pub comments: Option<String>,
}
#[allow(dead_code)]
impl ContractSigner {
/// Creates a new contract signer
pub fn new(name: String, email: String) -> Self {
@ -123,6 +124,7 @@ pub struct ContractRevision {
pub comments: Option<String>,
}
#[allow(dead_code)]
impl ContractRevision {
/// Creates a new contract revision
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
@ -166,6 +168,7 @@ pub struct Contract {
pub toc: Option<Vec<TocItem>>,
}
#[allow(dead_code)]
impl Contract {
/// Creates a new contract
pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {

View File

@ -14,6 +14,7 @@ pub enum DefiPositionStatus {
Cancelled
}
#[allow(dead_code)]
impl DefiPositionStatus {
pub fn as_str(&self) -> &str {
match self {
@ -35,6 +36,7 @@ pub enum DefiPositionType {
Collateral,
}
#[allow(dead_code)]
impl DefiPositionType {
pub fn as_str(&self) -> &str {
match self {
@ -95,6 +97,7 @@ pub struct DefiDatabase {
receiving_positions: HashMap<String, ReceivingPosition>,
}
#[allow(dead_code)]
impl DefiDatabase {
pub fn new() -> Self {
Self {

View File

@ -110,6 +110,7 @@ pub struct FlowStep {
pub logs: Vec<FlowLog>,
}
#[allow(dead_code)]
impl FlowStep {
/// Creates a new flow step
pub fn new(name: String, description: String, order: u32) -> Self {
@ -189,6 +190,7 @@ pub struct FlowLog {
pub timestamp: DateTime<Utc>,
}
#[allow(dead_code)]
impl FlowLog {
/// Creates a new flow log
pub fn new(message: String) -> Self {
@ -231,6 +233,7 @@ pub struct Flow {
pub current_step: Option<FlowStep>,
}
#[allow(dead_code)]
impl Flow {
/// Creates a new flow
pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self {

View File

@ -1,248 +0,0 @@
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Represents the status of a governance proposal
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProposalStatus {
/// Proposal is in draft status, not yet open for voting
Draft,
/// Proposal is active and open for voting
Active,
/// Proposal has been approved by the community
Approved,
/// Proposal has been rejected by the community
Rejected,
/// Proposal has been cancelled by the creator
Cancelled,
}
impl std::fmt::Display for ProposalStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProposalStatus::Draft => write!(f, "Draft"),
ProposalStatus::Active => write!(f, "Active"),
ProposalStatus::Approved => write!(f, "Approved"),
ProposalStatus::Rejected => write!(f, "Rejected"),
ProposalStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
/// Represents a vote on a governance proposal
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum VoteType {
/// Vote in favor of the proposal
Yes,
/// Vote against the proposal
No,
/// Abstain from voting on the proposal
Abstain,
}
impl std::fmt::Display for VoteType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VoteType::Yes => write!(f, "Yes"),
VoteType::No => write!(f, "No"),
VoteType::Abstain => write!(f, "Abstain"),
}
}
}
/// Represents a governance proposal in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Proposal {
/// Unique identifier for the proposal
pub id: String,
/// User ID of the proposal creator
pub creator_id: i32,
/// Name of the proposal creator
pub creator_name: String,
/// Title of the proposal
pub title: String,
/// Detailed description of the proposal
pub description: String,
/// Current status of the proposal
pub status: ProposalStatus,
/// Date and time when the proposal was created
pub created_at: DateTime<Utc>,
/// Date and time when the proposal was last updated
pub updated_at: DateTime<Utc>,
/// Date and time when voting starts
pub voting_starts_at: Option<DateTime<Utc>>,
/// Date and time when voting ends
pub voting_ends_at: Option<DateTime<Utc>>,
}
impl Proposal {
/// Creates a new proposal
pub fn new(creator_id: i32, creator_name: String, title: String, description: String) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
creator_id,
creator_name,
title,
description,
status: ProposalStatus::Draft,
created_at: now,
updated_at: now,
voting_starts_at: None,
voting_ends_at: None,
}
}
/// Updates the proposal status
pub fn update_status(&mut self, status: ProposalStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Sets the voting period for the proposal
pub fn set_voting_period(&mut self, starts_at: DateTime<Utc>, ends_at: DateTime<Utc>) {
self.voting_starts_at = Some(starts_at);
self.voting_ends_at = Some(ends_at);
self.updated_at = Utc::now();
}
/// Activates the proposal for voting
pub fn activate(&mut self) {
self.status = ProposalStatus::Active;
self.updated_at = Utc::now();
}
/// Cancels the proposal
pub fn cancel(&mut self) {
self.status = ProposalStatus::Cancelled;
self.updated_at = Utc::now();
}
}
/// Represents a vote cast on a proposal
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
/// Unique identifier for the vote
pub id: String,
/// ID of the proposal being voted on
pub proposal_id: String,
/// User ID of the voter
pub voter_id: i32,
/// Name of the voter
pub voter_name: String,
/// Type of vote cast
pub vote_type: VoteType,
/// Optional comment explaining the vote
pub comment: Option<String>,
/// Date and time when the vote was cast
pub created_at: DateTime<Utc>,
/// Date and time when the vote was last updated
pub updated_at: DateTime<Utc>,
}
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 {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
proposal_id,
voter_id,
voter_name,
vote_type,
comment,
created_at: now,
updated_at: now,
}
}
/// Updates the vote type
pub fn update_vote(&mut self, vote_type: VoteType, comment: Option<String>) {
self.vote_type = vote_type;
self.comment = comment;
self.updated_at = Utc::now();
}
}
/// Represents a filter for searching proposals
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposalFilter {
/// Filter by proposal status
pub status: Option<String>,
/// Filter by creator ID
pub creator_id: Option<i32>,
/// Search term for title and description
pub search: Option<String>,
}
impl Default for ProposalFilter {
fn default() -> Self {
Self {
status: None,
creator_id: None,
search: None,
}
}
}
/// Represents the voting results for a proposal
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VotingResults {
/// Proposal ID
pub proposal_id: String,
/// Number of yes votes
pub yes_count: usize,
/// Number of no votes
pub no_count: usize,
/// Number of abstain votes
pub abstain_count: usize,
/// Total number of votes
pub total_votes: usize,
}
impl VotingResults {
/// Creates a new empty voting results object
pub fn new(proposal_id: String) -> Self {
Self {
proposal_id,
yes_count: 0,
no_count: 0,
abstain_count: 0,
total_votes: 0,
}
}
/// Adds a vote to the results
pub fn add_vote(&mut self, vote_type: &VoteType) {
match vote_type {
VoteType::Yes => self.yes_count += 1,
VoteType::No => self.no_count += 1,
VoteType::Abstain => self.abstain_count += 1,
}
self.total_votes += 1;
}
/// Calculates the percentage of yes votes
pub fn yes_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.yes_count as f64 / self.total_votes as f64) * 100.0
}
/// Calculates the percentage of no votes
pub fn no_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.no_count as f64 / self.total_votes as f64) * 100.0
}
/// Calculates the percentage of abstain votes
pub fn abstain_percentage(&self) -> f64 {
if self.total_votes == 0 {
return 0.0;
}
(self.abstain_count as f64 / self.total_votes as f64) * 100.0
}
}

View File

@ -1,7 +1,7 @@
use crate::models::asset::AssetType;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::models::asset::{Asset, AssetType};
/// Status of a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -12,6 +12,7 @@ pub enum ListingStatus {
Expired,
}
#[allow(dead_code)]
impl ListingStatus {
pub fn as_str(&self) -> &str {
match self {
@ -63,6 +64,7 @@ pub enum BidStatus {
Cancelled,
}
#[allow(dead_code)]
impl BidStatus {
pub fn as_str(&self) -> &str {
match self {
@ -103,6 +105,7 @@ pub struct Listing {
pub image_url: Option<String>,
}
#[allow(dead_code)]
impl Listing {
/// Creates a new listing
pub fn new(
@ -150,7 +153,13 @@ impl Listing {
}
/// Adds a bid to the listing
pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> {
pub fn add_bid(
&mut self,
bidder_id: String,
bidder_name: String,
amount: f64,
currency: String,
) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
@ -160,7 +169,10 @@ impl Listing {
}
if currency != self.currency {
return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency));
return Err(format!(
"Currency mismatch: expected {}, got {}",
self.currency, currency
));
}
// Check if bid amount is higher than current highest bid or starting price
@ -193,13 +205,19 @@ impl Listing {
/// Gets the highest bid on the listing
pub fn highest_bid(&self) -> Option<&Bid> {
self.bids.iter()
self.bids
.iter()
.filter(|b| b.status == BidStatus::Active)
.max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
}
/// Marks the listing as sold
pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> {
pub fn mark_as_sold(
&mut self,
buyer_id: String,
buyer_name: String,
sale_price: f64,
) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
@ -257,11 +275,13 @@ impl MarketplaceStatistics {
let mut listings_by_type = std::collections::HashMap::new();
let mut sales_by_asset_type = std::collections::HashMap::new();
let active_listings = listings.iter()
let active_listings = listings
.iter()
.filter(|l| l.status == ListingStatus::Active)
.count();
let sold_listings = listings.iter()
let sold_listings = listings
.iter()
.filter(|l| l.status == ListingStatus::Sold)
.count();

View File

@ -1,17 +1,16 @@
// Export models
pub mod user;
pub mod ticket;
pub mod calendar;
pub mod governance;
pub mod flow;
pub mod contract;
pub mod asset;
pub mod marketplace;
pub mod calendar;
pub mod contract;
pub mod defi;
pub mod flow;
pub mod marketplace;
pub mod ticket;
pub mod user;
// Re-export models for easier imports
pub use calendar::CalendarViewMode;
pub use defi::initialize_mock_data;
pub use ticket::{Ticket, TicketComment, TicketPriority, TicketStatus};
pub use user::User;
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
pub use calendar::{CalendarEvent, CalendarViewMode};
pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
pub use defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB, initialize_mock_data};

View File

@ -76,6 +76,7 @@ pub struct Ticket {
pub assigned_to: Option<i32>,
}
#[allow(dead_code)]
impl Ticket {
/// Creates a new ticket
pub fn new(user_id: i32, title: String, description: String, priority: TicketPriority) -> Self {

View File

@ -4,6 +4,7 @@ use bcrypt::{hash, verify, DEFAULT_COST};
/// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct User {
/// Unique identifier for the user
pub id: Option<i32>,
@ -31,6 +32,7 @@ pub enum UserRole {
Admin,
}
#[allow(dead_code)]
impl User {
/// Creates a new user with default values
pub fn new(name: String, email: String) -> Self {
@ -125,6 +127,7 @@ impl User {
/// Represents user login credentials
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LoginCredentials {
pub email: String,
pub password: String,
@ -132,6 +135,7 @@ pub struct LoginCredentials {
/// Represents user registration data
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct RegistrationData {
pub name: String,
pub email: String,

View File

@ -1,26 +1,24 @@
use actix_web::web;
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use crate::controllers::home::HomeController;
use crate::controllers::auth::AuthController;
use crate::controllers::ticket::TicketController;
use crate::controllers::calendar::CalendarController;
use crate::controllers::governance::GovernanceController;
use crate::controllers::flow::FlowController;
use crate::controllers::contract::ContractController;
use crate::controllers::asset::AssetController;
use crate::controllers::marketplace::MarketplaceController;
use crate::controllers::defi::DefiController;
use crate::controllers::company::CompanyController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
use crate::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()
)
let session_middleware =
SessionMiddleware::builder(CookieSessionStore::default(), SESSION_KEY.clone())
.cookie_secure(false) // Set to true in production with HTTPS
.build();
@ -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("/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(
"/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),
),
)
// 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
);
}

View File

@ -1,16 +1,17 @@
use actix_web::{error, Error, HttpResponse};
use actix_web::{Error, HttpResponse};
use chrono::{DateTime, Utc};
use tera::{self, Context, Function, Tera, Value};
use std::error::Error as StdError;
use tera::{self, Context, Function, Tera, Value};
// Export modules
pub mod redis_service;
// Re-export for easier imports
pub use redis_service::RedisCalendarService;
// pub use redis_service::RedisCalendarService; // Currently unused
/// Error type for template rendering
#[derive(Debug)]
#[allow(dead_code)]
pub struct TemplateError {
pub message: String,
pub details: String,
@ -25,10 +26,16 @@ impl std::fmt::Display for TemplateError {
impl std::error::Error for TemplateError {}
/// Registers custom Tera functions
/// Registers custom Tera functions and filters
pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction);
tera.register_function("format_date", FormatDateFunction);
tera.register_function("local_time", LocalTimeFunction);
// Register custom filters
tera.register_filter("format_hour", format_hour_filter);
tera.register_filter("extract_hour", extract_hour_filter);
tera.register_filter("format_time", format_time_filter);
}
/// Tera function to get the current date/time
@ -68,14 +75,10 @@ impl Function for FormatDateFunction {
None => {
return Err(tera::Error::msg(
"The 'timestamp' argument must be a valid timestamp",
))
));
}
},
None => {
return Err(tera::Error::msg(
"The 'timestamp' argument is required",
))
}
None => return Err(tera::Error::msg("The 'timestamp' argument is required")),
};
let format = match args.get("format") {
@ -89,23 +92,130 @@ impl Function for FormatDateFunction {
// Convert timestamp to DateTime using the non-deprecated method
let datetime = match DateTime::from_timestamp(timestamp, 0) {
Some(dt) => dt,
None => {
return Err(tera::Error::msg(
"Failed to convert timestamp to datetime",
))
}
None => return Err(tera::Error::msg("Failed to convert timestamp to datetime")),
};
Ok(Value::String(datetime.format(format).to_string()))
}
}
/// Tera function to convert UTC datetime to local time
#[derive(Clone)]
pub struct LocalTimeFunction;
impl Function for LocalTimeFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
let datetime_value = match args.get("datetime") {
Some(val) => val,
None => return Err(tera::Error::msg("The 'datetime' argument is required")),
};
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
None => "%Y-%m-%d %H:%M",
},
None => "%Y-%m-%d %H:%M",
};
// The datetime comes from Rust as a serialized DateTime<Utc>
// We need to handle it properly
let utc_datetime = if let Some(dt_str) = datetime_value.as_str() {
// Try to parse as RFC3339 first
match DateTime::parse_from_rfc3339(dt_str) {
Ok(dt) => dt.with_timezone(&Utc),
Err(_) => {
// Try to parse as our standard format
match DateTime::parse_from_str(dt_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
Ok(dt) => dt.with_timezone(&Utc),
Err(_) => return Err(tera::Error::msg("Invalid datetime string format")),
}
}
}
} else {
return Err(tera::Error::msg("Datetime must be a string"));
};
// Convert UTC to local time (EEST = UTC+3)
// In a real application, you'd want to get the user's timezone from their profile
let local_offset = chrono::FixedOffset::east_opt(3 * 3600).unwrap(); // +3 hours for EEST
let local_datetime = utc_datetime.with_timezone(&local_offset);
Ok(Value::String(local_datetime.format(format).to_string()))
}
}
/// Tera filter to format hour with zero padding
pub fn format_hour_filter(
value: &Value,
_args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
match value.as_i64() {
Some(hour) => Ok(Value::String(format!("{:02}", hour))),
None => Err(tera::Error::msg("Value must be a number")),
}
}
/// Tera filter to extract hour from datetime string
pub fn extract_hour_filter(
value: &Value,
_args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
match value.as_str() {
Some(datetime_str) => {
// Try to parse as RFC3339 first
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
Ok(Value::String(dt.format("%H").to_string()))
} else {
// Try to parse as our standard format
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
Ok(dt) => Ok(Value::String(dt.format("%H").to_string())),
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
}
}
}
None => Err(tera::Error::msg("Value must be a string")),
}
}
/// Tera filter to format time from datetime string
pub fn format_time_filter(
value: &Value,
args: &std::collections::HashMap<String, Value>,
) -> tera::Result<Value> {
let format = match args.get("format") {
Some(val) => match val.as_str() {
Some(s) => s,
None => "%H:%M",
},
None => "%H:%M",
};
match value.as_str() {
Some(datetime_str) => {
// Try to parse as RFC3339 first
if let Ok(dt) = DateTime::parse_from_rfc3339(datetime_str) {
Ok(Value::String(dt.format(format).to_string()))
} else {
// Try to parse as our standard format
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S%.f UTC") {
Ok(dt) => Ok(Value::String(dt.format(format).to_string())),
Err(_) => Err(tera::Error::msg("Invalid datetime string format")),
}
}
}
None => Err(tera::Error::msg("Value must be a string")),
}
}
/// Formats a date for display
#[allow(dead_code)]
pub fn format_date(date: &DateTime<Utc>, format: &str) -> String {
date.format(format).to_string()
}
/// Truncates a string to a maximum length and adds an ellipsis if truncated
#[allow(dead_code)]
pub fn truncate_string(s: &str, max_length: usize) -> String {
if s.len() <= max_length {
s.to_string()
@ -136,10 +246,13 @@ pub fn render_template(
Ok(content) => {
println!("DEBUG: Successfully rendered template: {}", template_name);
Ok(HttpResponse::Ok().content_type("text/html").body(content))
},
}
Err(e) => {
// Log the error with more details
println!("DEBUG: Template rendering error for {}: {}", template_name, e);
println!(
"DEBUG: Template rendering error for {}: {}",
template_name, e
);
println!("DEBUG: Error details: {:?}", e);
// Print the error cause chain for better debugging

View File

@ -1,7 +1,7 @@
use heromodels::models::Event as CalendarEvent;
use lazy_static::lazy_static;
use redis::{Client, Commands, Connection, RedisError};
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use crate::models::CalendarEvent;
// Create a lazy static Redis client that can be used throughout the application
lazy_static! {
@ -59,11 +59,11 @@ impl RedisCalendarService {
})?;
// Save the event
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id);
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.base_data.id);
let _: () = conn.set(event_key, json)?;
// Add the event ID to the set of all events
let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.id)?;
let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.base_data.id)?;
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
</div>
{% endif %}
<form action="/calendar/new" method="post">
<form action="/calendar/events" method="post">
<div class="mb-3">
<label for="title" class="form-label">Event Title</label>
<input type="text" class="form-control" id="title" name="title" required>
@ -39,6 +39,13 @@
</div>
</div>
<!-- Show selected date info when coming from calendar date click -->
<div id="selected-date-info" class="alert alert-info" style="display: none;">
<strong>Selected Date:</strong> <span id="selected-date-display"></span>
<br>
<small>The date is pre-selected. You can only modify the time portion.</small>
</div>
<div class="mb-3">
<label for="color" class="form-label">Event Color</label>
<select class="form-control" id="color" name="color">
@ -60,6 +67,69 @@
<script>
document.addEventListener('DOMContentLoaded', function () {
// Check if we came from a date click (URL parameter)
const urlParams = new URLSearchParams(window.location.search);
const selectedDate = urlParams.get('date');
if (selectedDate) {
// Show the selected date info
document.getElementById('selected-date-info').style.display = 'block';
document.getElementById('selected-date-display').textContent = new Date(selectedDate).toLocaleDateString();
// Pre-fill the date portion and restrict date changes
const startTimeInput = document.getElementById('start_time');
const endTimeInput = document.getElementById('end_time');
// Set default times (9 AM to 10 AM on the selected date)
const startDateTime = new Date(selectedDate + 'T09:00');
const endDateTime = new Date(selectedDate + 'T10:00');
// Format for datetime-local input (YYYY-MM-DDTHH:MM)
startTimeInput.value = startDateTime.toISOString().slice(0, 16);
endTimeInput.value = endDateTime.toISOString().slice(0, 16);
// Set minimum and maximum date to the selected date to prevent changing the date
const minDate = selectedDate + 'T00:00';
const maxDate = selectedDate + 'T23:59';
startTimeInput.min = minDate;
startTimeInput.max = maxDate;
endTimeInput.min = minDate;
endTimeInput.max = maxDate;
// Add event listeners to ensure end time is after start time
startTimeInput.addEventListener('change', function () {
const startTime = new Date(this.value);
const endTime = new Date(endTimeInput.value);
if (endTime <= startTime) {
// Set end time to 1 hour after start time
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
endTimeInput.value = newEndTime.toISOString().slice(0, 16);
}
// Update end time minimum to be after start time
endTimeInput.min = this.value;
});
endTimeInput.addEventListener('change', function () {
const startTime = new Date(startTimeInput.value);
const endTime = new Date(this.value);
if (endTime <= startTime) {
// Reset to 1 hour after start time
const newEndTime = new Date(startTime.getTime() + 60 * 60 * 1000);
this.value = newEndTime.toISOString().slice(0, 16);
}
});
} else {
// No date selected, set default to current time
const now = new Date();
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
document.getElementById('start_time').value = now.toISOString().slice(0, 16);
document.getElementById('end_time').value = oneHourLater.toISOString().slice(0, 16);
}
// Convert datetime-local inputs to RFC3339 format on form submission
document.querySelector('form').addEventListener('submit', function (e) {
e.preventDefault();
@ -67,6 +137,12 @@
const startTime = document.getElementById('start_time').value;
const endTime = document.getElementById('end_time').value;
// Validate that end time is after start time
if (new Date(endTime) <= new Date(startTime)) {
alert('End time must be after start time');
return;
}
// Convert to RFC3339 format
const startRFC = new Date(startTime).toISOString();
const endRFC = new Date(endTime).toISOString();

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

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

View 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.created_at | 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 %}

View File

@ -4,53 +4,54 @@
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Create Governance Proposal</h1>
<p class="lead">Submit a new proposal for the community to vote on.</p>
</div>
</div>
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs -->
<div class="row mb-4">
{% include "governance/_tabs.html" %}
<!-- Info Alert -->
<div class="row">
<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 class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Creating Proposals</h5>
<p>Creating a proposal is an important step in our community governance process. Well-crafted proposals
clearly state the problem, solution, and implementation details. The community will review and vote
on your proposal, so be thorough and thoughtful in your submission.</p>
<div class="mt-2">
<a href="/governance/proposal-templates" class="btn btn-sm btn-outline-primary"><i
class="bi bi-file-earmark-text"></i> Proposal Templates</a>
</div>
</div>
</div>
</div>
<!-- Proposal Form -->
<!-- Proposal Form and Guidelines in Flex Layout -->
<div class="row mb-4">
<div class="col-md-8 mx-auto">
<div class="card">
<!-- Proposal Form Column -->
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">New Proposal</h5>
</div>
<div class="card-body">
<form action="/governance/create" method="post">
<form action="/governance/create" method="post" id="proposalForm" novalidate>
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required
placeholder="Enter a clear, concise title for your proposal">
<input type="text" class="form-control" id="title" name="title" required minlength="5"
maxlength="100" placeholder="Enter a clear, concise title for your proposal">
<div class="invalid-feedback">Please provide a title (5-100 characters).</div>
<div class="form-text">Make it descriptive and specific</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="6" required
<textarea class="form-control" id="description" name="description" rows="8" required
minlength="50" maxlength="5000"
placeholder="Provide a detailed description of your proposal..."></textarea>
<div class="invalid-feedback">Please provide a detailed description (at least 50
characters).</div>
<div class="form-text">Explain the purpose, benefits, and implementation details</div>
</div>
@ -58,11 +59,15 @@
<div class="col-md-6">
<label for="voting_start_date" class="form-label">Voting Start Date</label>
<input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
<div class="invalid-feedback" id="start_date_feedback">Please select a valid start date.
</div>
<div class="form-text">When should voting begin?</div>
</div>
<div class="col-md-6">
<label for="voting_end_date" class="form-label">Voting End Date</label>
<input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
<div class="invalid-feedback" id="end_date_feedback">End date must be after start date.
</div>
<div class="form-text">When should voting end?</div>
</div>
</div>
@ -84,12 +89,10 @@
</div>
</div>
</div>
</div>
<!-- Guidelines Card -->
<div class="row mb-4">
<div class="col-md-8 mx-auto">
<div class="card bg-light">
<!-- Guidelines Column -->
<div class="col-lg-4">
<div class="card bg-light h-100">
<div class="card-header">
<h5 class="mb-0">Proposal Guidelines</h5>
</div>
@ -116,4 +119,111 @@
</div>
</div>
</div>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('proposalForm');
const startDateInput = document.getElementById('voting_start_date');
const endDateInput = document.getElementById('voting_end_date');
const startDateFeedback = document.getElementById('start_date_feedback');
const endDateFeedback = document.getElementById('end_date_feedback');
// Set default dates
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const nextWeek = new Date(today);
nextWeek.setDate(nextWeek.getDate() + 7);
// Format dates for input fields
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Set default values
startDateInput.value = formatDate(tomorrow);
endDateInput.value = formatDate(nextWeek);
// Validate dates when they change
function validateDates() {
const startDate = new Date(startDateInput.value);
const endDate = new Date(endDateInput.value);
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0); // Reset time to start of day
let startValid = true;
let endValid = true;
// Validate start date is not in the past
if (startDate < currentDate) {
startDateInput.classList.add('is-invalid');
startDateFeedback.textContent = 'Start date cannot be in the past.';
startValid = false;
} else {
startDateInput.classList.remove('is-invalid');
}
// Validate end date is after start date
if (endDate < startDate) {
endDateInput.classList.add('is-invalid');
endDateFeedback.textContent = 'End date must be after start date.';
endValid = false;
} else {
endDateInput.classList.remove('is-invalid');
}
return startValid && endValid;
}
// Validate on input
startDateInput.addEventListener('change', validateDates);
endDateInput.addEventListener('change', validateDates);
// Form submission validation
form.addEventListener('submit', function (event) {
let formValid = true;
// Validate required fields
const requiredFields = form.querySelectorAll('[required]');
requiredFields.forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
formValid = false;
} else {
field.classList.remove('is-invalid');
}
// Check minlength if specified
if (field.minLength && field.value.length < field.minLength) {
field.classList.add('is-invalid');
formValid = false;
}
});
// Validate dates
const datesValid = validateDates();
formValid = formValid && datesValid;
// If form is not valid, prevent submission
if (!formValid) {
event.preventDefault();
// Scroll to the first invalid element
const firstInvalid = form.querySelector('.is-invalid');
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstInvalid.focus();
}
}
});
// Initial validation
validateDates();
});
</script>
{% endblock %}
{% endblock %}

View File

@ -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">
@ -29,9 +15,12 @@
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Governance</h5>
<p>The governance system allows token holders to participate in decision-making processes by voting on proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction of our decentralized ecosystem.</p>
<p>The governance system allows token holders to participate in decision-making processes by voting on
proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction
of our decentralized ecosystem.</p>
<div class="mt-2">
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i>
Read Documentation</a>
</div>
</div>
</div>
@ -46,8 +35,10 @@
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Urgent: Voting Closes Soon</h5>
<div>
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
<a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a>
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.vote_end_date |
date(format="%Y-%m-%d") }}</span>
<a href="/governance/proposals/{{ nearest_proposal.base_data.id }}"
class="btn btn-sm btn-outline-primary">View Full Proposal</a>
</div>
</div>
<div class="card-body">
@ -58,26 +49,54 @@
<p>{{ nearest_proposal.description }}</p>
</div>
{% set yes_percent = 0 %}
{% set no_percent = 0 %}
{% set abstain_percent = 0 %}
{% set total_votes = 0 %}
{% if nearest_proposal_results is defined %}
{% if nearest_proposal_results.total_votes > 0 %}
{% set yes_percent = (nearest_proposal_results.yes_count * 100 / nearest_proposal_results.total_votes) |
int %}
{% set no_percent = (nearest_proposal_results.no_count * 100 / nearest_proposal_results.total_votes) |
int %}
{% set abstain_percent = (nearest_proposal_results.abstain_count * 100 /
nearest_proposal_results.total_votes) |
int %}
{% endif %}
{% set total_votes = nearest_proposal_results.total_votes %}
{% endif %}
<div class="progress mb-3" style="height: 25px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">65% Yes</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div>
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100">{{ yes_percent }}% Yes
</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100">{{ no_percent }}% No
</div>
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"
aria-valuenow="{{ abstain_percent }}" aria-valuemin="0" aria-valuemax="100">{{ abstain_percent
}}% Abstain
</div>
</div>
<div class="d-flex justify-content-between text-muted small mb-4">
<span>26 votes cast</span>
<span>Quorum: 75% reached</span>
<span>{{ total_votes }} votes cast</span>
<span>Quorum: {% if total_votes >= 20 %}75% reached{% else %}Not reached{% endif %}</span>
</div>
<div class="mb-4">
<h5 class="mb-3">Cast Your Vote</h5>
<form>
<form action="/governance/proposals/{{ nearest_proposal.base_data.id }}/vote" method="post">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment">
<input type="text" class="form-control" name="comment"
placeholder="Optional comment on your vote" aria-label="Vote comment">
</div>
<div class="d-flex justify-content-between">
<button type="submit" name="vote" value="yes" class="btn btn-success">Vote Yes</button>
<button type="submit" name="vote" value="no" class="btn btn-danger">Vote No</button>
<button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button>
<button type="submit" name="vote_type" value="Yes" class="btn btn-success">Vote Yes</button>
<button type="submit" name="vote_type" value="No" class="btn btn-danger">Vote No</button>
<button type="submit" name="vote_type" value="Abstain"
class="btn btn-secondary">Abstain</button>
</div>
</form>
</div>
@ -112,9 +131,11 @@
<div>
<div class="d-flex justify-content-between align-items-center">
<strong>{{ activity.user }}</strong>
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small>
<small class="text-muted">{{ activity.created_at | date(format="%H:%M") }}</small>
</div>
<p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p>
<p class="mb-1">{{ activity.action }} on <a
href="/governance/proposals/{{ activity.proposal_id }}">{{
activity.proposal_title }}</a></p>
{% if activity.type == "comment" and activity.comment is defined %}
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
{% endif %}
@ -125,7 +146,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>
@ -142,22 +163,23 @@
<div class="row">
{% set count = 0 %}
{% for proposal in proposals %}
{% if count < 3 %}
<div class="col-md-4 mb-3">
{% if count < 3 %} <div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ proposal.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
<p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
<span
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ proposal.status }}
</span>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
<a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-outline-primary">View Details</a>
</div>
</div>
<div class="card-footer text-muted text-center">
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
<span>Voting ends: {{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</span>
</div>
</div>
</div>

View File

@ -3,23 +3,60 @@
{% block title %}My Votes - Governance Dashboard{% endblock %}
{% block content %}
<!-- Header -->
{% include "governance/_header.html" %}
<!-- Navigation Tabs -->
<div class="row mb-4">
{% include "governance/_tabs.html" %}
<!-- Info Alert -->
<div class="row">
<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 class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Votes</h5>
<p>Voting is a fundamental right of all token holders in our governance system. Each vote carries weight
proportional to your token holdings, ensuring fair representation. The voting statistics below show the
community's collective decision-making across all proposals.</p>
<div class="mt-2">
<a href="/governance/voting-guide" class="btn btn-sm btn-outline-primary"><i
class="bi bi-check2-square"></i> Voting Guide</a>
</div>
</div>
</div>
</div>
<!-- Voting Stats -->
<div class="row mb-4">
<div class="col-md-4 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body text-center">
<h5 class="card-title">Yes Votes</h5>
<p class="display-4">
{{ total_yes_votes }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body text-center">
<h5 class="card-title">No Votes</h5>
<p class="display-4">
{{ total_no_votes }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body text-center">
<h5 class="card-title">Abstain Votes</h5>
<p class="display-4">
{{ total_abstain_votes }}
</p>
</div>
</div>
</div>
</div>
@ -48,18 +85,21 @@
<tr>
<td>{{ proposal.title }}</td>
<td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
<span
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ vote.vote_type }}
</span>
</td>
<td>
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
<span
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
{{ proposal.status }}
</span>
</td>
<td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View Proposal</a>
<a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-primary">View Proposal</a>
</td>
</tr>
{% endfor %}
@ -79,57 +119,5 @@
</div>
</div>
<!-- Voting Stats -->
{% if votes | length > 0 %}
<div class="row mb-4">
<div class="col-md-4 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body text-center">
<h5 class="card-title">Yes Votes</h5>
<p class="display-4">
{% set yes_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Yes' %}
{% set yes_count = yes_count + 1 %}
{% endif %}
{% endfor %}
{{ yes_count }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body text-center">
<h5 class="card-title">No Votes</h5>
<p class="display-4">
{% set no_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'No' %}
{% set no_count = no_count + 1 %}
{% endif %}
{% endfor %}
{{ no_count }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body text-center">
<h5 class="card-title">Abstain Votes</h5>
<p class="display-4">
{% set abstain_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Abstain' %}
{% set abstain_count = abstain_count + 1 %}
{% endif %}
{% endfor %}
{{ abstain_count }}
</p>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -2,8 +2,45 @@
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
{% block styles %}
<style>
.avatar-circle {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.comment-text {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.comment-text:hover {
white-space: normal;
overflow: visible;
}
.progress {
border-radius: 10px;
overflow: hidden;
}
</style>
{% endblock %}
{% 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">
@ -30,42 +67,62 @@
<!-- Proposal Details -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<div class="col-lg-8">
<div class="card h-100 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">{{ proposal.title }}</h4>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-center mb-3">
<span
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
<i
class="bi {% if proposal.status == 'Active' %}bi-check-circle{% elif proposal.status == 'Approved' %}bi-trophy{% elif proposal.status == 'Rejected' %}bi-x-circle{% elif proposal.status == 'Draft' %}bi-pencil{% else %}bi-exclamation-circle{% endif %} me-1"></i>
{{ proposal.status }}
</span>
<small class="text-muted">Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }}</small>
<span class="text-muted"><i class="bi bi-person me-1"></i>Created by {{ proposal.creator_name
}}</span>
</div>
<h5>Description</h5>
<p class="mb-4">{{ proposal.description }}</p>
<div class="flex-grow-1">
<h5><i class="bi bi-file-text me-2"></i>Description</h5>
<div class="p-3 bg-light rounded mb-4">{{ proposal.description }}</div>
</div>
<h5>Voting Period</h5>
<p>
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
<div class="mt-auto">
<h5><i class="bi bi-calendar-event me-2"></i>Voting Period</h5>
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
{% if proposal.vote_start_date and proposal.vote_end_date %}
<div>
<div class="text-muted mb-1">Start Date</div>
<div class="fw-bold">{{ proposal.vote_start_date | date(format="%Y-%m-%d") }}</div>
</div>
<div class="text-center">
<i class="bi bi-arrow-right fs-4 text-muted"></i>
</div>
<div>
<div class="text-muted mb-1">End Date</div>
<div class="fw-bold">{{ proposal.vote_end_date | date(format="%Y-%m-%d") }}</div>
</div>
{% else %}
Not set
<div class="text-center w-100">Not set</div>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Voting Results</h5>
<div class="col-lg-4">
<div class="card mb-4 shadow-sm h-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-bar-chart-fill me-2"></i>Voting Dashboard</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="card-body d-flex flex-column">
<!-- Voting Results Section -->
<div class="mb-4">
<h6 class="border-bottom pb-2 mb-3">Results</h6>
{% set yes_percent = 0 %}
{% set no_percent = 0 %}
{% set abstain_percent = 0 %}
@ -76,114 +133,483 @@
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
{% endif %}
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div>
<!-- Yes votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-success"><i class="bi bi-check-circle-fill me-1"></i> Yes</span>
<span class="badge bg-success rounded-pill">{{ results.yes_count }}</span>
</div>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"
aria-valuenow="{{ yes_percent }}" aria-valuemin="0" aria-valuemax="100"
title="{{ yes_percent }}% of votes"></div>
</div>
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div>
<!-- No votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-danger"><i class="bi bi-x-circle-fill me-1"></i> No</span>
<span class="badge bg-danger rounded-pill">{{ results.no_count }}</span>
</div>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"
aria-valuenow="{{ no_percent }}" aria-valuemin="0" aria-valuemax="100"
title="{{ no_percent }}% of votes"></div>
</div>
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div>
<!-- Abstain votes -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-secondary"><i class="bi bi-dash-circle-fill me-1"></i>
Abstain</span>
<span class="badge bg-secondary rounded-pill">{{ results.abstain_count }}</span>
</div>
</div>
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
<div class="progress mb-3" style="height: 12px;">
<div class="progress-bar bg-secondary" role="progressbar"
style="width: {{ abstain_percent }}%" aria-valuenow="{{ abstain_percent }}"
aria-valuemin="0" aria-valuemax="100" title="{{ abstain_percent }}% of votes"></div>
</div>
</div>
<!-- Vote Form -->
{% if proposal.status == "Active" and user and user.id %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">Cast Your Vote</h5>
<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded">
<div class="text-center">
<h4 class="mb-0">{{ results.total_votes }}</h4>
<small class="text-muted">Total Votes</small>
</div>
<div class="card-body">
<form action="/governance/proposals/{{ proposal.id }}/vote" method="post">
<div class="mb-3">
<label class="form-label">Vote Type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked>
<label class="form-check-label" for="voteYes">
Yes - I support this proposal
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
<label class="form-check-label" for="voteNo">
No - I oppose this proposal
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain" value="Abstain">
<label class="form-check-label" for="voteAbstain">
Abstain - I choose not to vote
</label>
</div>
</div>
<div class="mb-3">
<label for="comment" class="form-label">Comment (Optional)</label>
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="Explain your vote..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
</form>
</div>
</div>
{% elif not user or not user.id %}
<div class="card">
<div class="card-body text-center">
<p>You must be logged in to vote.</p>
<a href="/login" class="btn btn-primary">Login to Vote</a>
{% if proposal.status == "Active" %}
<div class="text-center">
<div class="position-relative d-inline-block" style="width: 60px; height: 60px;">
<svg width="60" height="60">
<circle cx="30" cy="30" r="25" fill="none" stroke="#e9ecef" stroke-width="5">
</circle>
<circle cx="30" cy="30" r="25" fill="none" stroke="#0d6efd" stroke-width="5"
stroke-dasharray="157"
stroke-dashoffset="{{ 157 - (157 * yes_percent / 100) }}"
transform="rotate(-90 30 30)"></circle>
</svg>
<div
class="position-absolute top-50 start-50 translate-middle text-primary fw-bold">
{{ yes_percent }}%</div>
</div>
<small class="text-muted">Approval Rate</small>
</div>
{% endif %}
</div>
</div>
<!-- Vote Form Section -->
{% 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"
id="voteForm">
<div class="mb-3">
<div class="d-flex gap-2 mb-2">
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes"
value="Yes" required>
<label class="form-check-label text-success" for="voteYes"><i
class="bi bi-check-circle-fill me-1"></i>Yes</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteNo"
value="No">
<label class="form-check-label text-danger" for="voteNo"><i
class="bi bi-x-circle-fill me-1"></i>No</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain"
value="Abstain">
<label class="form-check-label text-secondary" for="voteAbstain"><i
class="bi bi-dash-circle-fill me-1"></i>Abstain</label>
</div>
</div>
<textarea class="form-control" id="comment" name="comment" rows="2"
placeholder="Add your thoughts about this proposal (optional)..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-send me-2"></i>Submit
Vote</button>
</form>
</div>
{% elif proposal.status != "Active" %}
<div class="mt-auto text-center p-3 bg-light rounded">
<i class="bi bi-info-circle fs-4 text-muted"></i>
<p class="mb-0 mt-2">Voting is {{ proposal.status | lower }} for this proposal</p>
</div>
{% elif not user or not user.id %}
<div class="mt-auto text-center p-3 bg-light rounded">
<i class="bi bi-person-lock fs-4 text-muted"></i>
<p class="mb-0 mt-2">You must be logged in to vote</p>
<a href="/login" class="btn btn-primary btn-sm mt-2">Login to Vote</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Votes List -->
<div class="row mb-4">
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Votes ({{ votes | length }})</h5>
<div class="card shadow-sm">
<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">
{% if votes | length > 0 %}
<div class="card-body p-0">
<div class="table-responsive">
<table class="table">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Voter</th>
<th class="ps-3">Voter</th>
<th>Vote</th>
<th>Comment</th>
<th>Date</th>
<th class="text-end pe-3">Date</th>
</tr>
</thead>
<tbody>
{% for vote in votes %}
<tbody id="votesTableBody">
{% if votes | length == 0 %}
<tr>
<td>{{ vote.voter_name }}</td>
<td colspan="4" class="text-center py-4">
<div class="py-3">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="mt-2 mb-0">No votes have been cast yet</p>
</div>
</td>
</tr>
{% else %}
{% for vote in votes %}
<tr class="vote-row" data-vote-type="{{ vote.vote_type | lower }}">
<td class="ps-3">
<div class="d-flex align-items-center">
<div class="avatar-circle me-2 bg-primary text-white">
U
</div>
<span>{{ vote.voter_name }}</span>
</div>
</td>
<td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
<span
class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %} rounded-pill px-3 py-2">
{% if vote.vote_type == 'Yes' %}
<i class="bi bi-check-circle-fill me-1"></i>
{% elif vote.vote_type == 'No' %}
<i class="bi bi-x-circle-fill me-1"></i>
{% else %}
<i class="bi bi-dash-circle-fill me-1"></i>
{% endif %}
{{ vote.vote_type }}
</span>
</td>
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
<td>
{% if vote.comment %}
<div class="comment-text">{{ vote.comment }}</div>
{% else %}
<span class="text-muted fst-italic">No comment provided</span>
{% endif %}
</td>
<td class="text-end pe-3">
<div class="d-flex flex-column align-items-end">
<span>{{ vote.created_at | date(format="%Y-%m-%d") }}</span>
<small class="text-muted">{{ vote.created_at | date(format="%H:%M")
}}</small>
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center">No votes have been cast yet.</p>
<!-- Pagination Controls -->
{% if votes | length > 10 %}
<div class="d-flex justify-content-between align-items-center p-3 border-top">
<div class="d-flex align-items-center">
<label class="me-2 text-muted small">Rows per page:</label>
<select id="rowsPerPage" class="form-select form-select-sm" style="width: auto;">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div>
<nav aria-label="Votes pagination">
<ul class="pagination pagination-sm mb-0" id="paginationControls">
<li class="page-item disabled" id="prevPage">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item" id="nextPage">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</div>
<div class="text-muted small" id="paginationInfo">
Showing <span id="startRow">1</span>-<span id="endRow">10</span> of <span
id="totalRows">{{ votes | length }}</span>
</div>
</div>
{% endif %}
</div>
</div>
</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);
// 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.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);
}
// 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();
}
// 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 (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 %}

View File

@ -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,33 +21,16 @@
</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>
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal
includes a detailed description, implementation plan, and voting period. Browse the list below to see all
active and past proposals.</p>
<div class="mt-2">
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i
class="bi bi-file-text"></i> Proposal Guidelines</a>
</div>
</div>
</div>
@ -55,17 +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="">All Statuses</option>
<option value="Draft">Draft</option>
<option value="Active">Active</option>
<option value="Approved">Approved</option>
<option value="Rejected">Rejected</option>
<option value="Cancelled">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">
<input type="text" class="form-control" id="search" name="search"
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>
@ -85,6 +80,7 @@
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
</div>
<div class="card-body">
{% if proposals and proposals|length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
@ -103,25 +99,41 @@
<td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td>
<td>
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
<span
class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
{{ proposal.status }}
</span>
</td>
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
<td>
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
{% if proposal.vote_start_date and proposal.vote_end_date %}
{{ proposal.vote_start_date | date(format="%Y-%m-%d") }} to {{
proposal.vote_end_date | date(format="%Y-%m-%d") }}
{% else %}
Not set
{% endif %}
</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
<a href="/governance/proposals/{{ proposal.base_data.id }}"
class="btn btn-sm btn-primary">View</a>
</td>
</tr>
{% 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>
{% endif %}
<a href="/governance/create" class="btn btn-primary mt-3">Create New Proposal</a>
</div>
{% endif %}
</div>
</div>
</div>