Compare commits
2 Commits
34594b95fa
...
b56f1cbc30
Author | SHA1 | Date | |
---|---|---|---|
|
b56f1cbc30 | ||
|
6060831f61 |
776
actix_mvc_app/src/controllers/asset.rs
Normal file
776
actix_mvc_app/src/controllers/asset.rs
Normal file
@ -0,0 +1,776 @@
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::asset::{Asset, AssetType, AssetStatus, BlockchainInfo, ValuationPoint, AssetTransaction, AssetStatistics};
|
||||
use crate::utils::render_template;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssetForm {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub asset_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ValuationForm {
|
||||
pub value: f64,
|
||||
pub currency: String,
|
||||
pub source: String,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TransactionForm {
|
||||
pub transaction_type: String,
|
||||
pub from_address: Option<String>,
|
||||
pub to_address: Option<String>,
|
||||
pub amount: Option<f64>,
|
||||
pub currency: Option<String>,
|
||||
pub transaction_hash: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AssetController;
|
||||
|
||||
impl AssetController {
|
||||
// Display the assets dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting assets dashboard rendering");
|
||||
|
||||
let assets = Self::get_mock_assets();
|
||||
println!("DEBUG: Generated {} mock assets", assets.len());
|
||||
|
||||
let stats = AssetStatistics::new(&assets);
|
||||
println!("DEBUG: Generated asset statistics: {:?}", stats);
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
// Add stats
|
||||
context.insert("stats", &serde_json::to_value(stats).unwrap());
|
||||
println!("DEBUG: Added stats to context");
|
||||
|
||||
// Add recent assets
|
||||
let recent_assets: Vec<serde_json::Map<String, serde_json::Value>> = assets
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|a| Self::asset_to_json(a))
|
||||
.collect();
|
||||
|
||||
context.insert("recent_assets", &recent_assets);
|
||||
|
||||
// Add assets by type
|
||||
let asset_types = vec![
|
||||
AssetType::NFT,
|
||||
AssetType::Token,
|
||||
AssetType::RealEstate,
|
||||
AssetType::Commodity,
|
||||
AssetType::Share,
|
||||
AssetType::Bond,
|
||||
AssetType::IntellectualProperty,
|
||||
AssetType::Other,
|
||||
];
|
||||
|
||||
let assets_by_type: Vec<serde_json::Map<String, serde_json::Value>> = asset_types
|
||||
.iter()
|
||||
.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();
|
||||
|
||||
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
|
||||
})
|
||||
.collect();
|
||||
|
||||
context.insert("assets_by_type", &assets_by_type);
|
||||
|
||||
println!("DEBUG: Rendering assets dashboard template");
|
||||
let response = render_template(&tmpl, "assets/index.html", &context);
|
||||
println!("DEBUG: Finished rendering assets dashboard template");
|
||||
response
|
||||
}
|
||||
|
||||
// Display the list of all assets
|
||||
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting assets list rendering");
|
||||
|
||||
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();
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
context.insert("assets", &assets_data);
|
||||
context.insert("filter", &"all");
|
||||
|
||||
println!("DEBUG: Rendering assets list template");
|
||||
let response = render_template(&tmpl, "assets/list.html", &context);
|
||||
println!("DEBUG: Finished rendering assets list template");
|
||||
response
|
||||
}
|
||||
|
||||
// Display the list of user's assets
|
||||
pub async fn my_assets(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting my assets rendering");
|
||||
|
||||
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();
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
context.insert("assets", &assets_data);
|
||||
|
||||
println!("DEBUG: Rendering my assets template");
|
||||
let response = render_template(&tmpl, "assets/my_assets.html", &context);
|
||||
println!("DEBUG: Finished rendering my assets template");
|
||||
response
|
||||
}
|
||||
|
||||
// Display a specific asset
|
||||
pub async fn detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
let asset_id = path.into_inner();
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting asset detail rendering");
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
// Find the asset by ID
|
||||
let assets = Self::get_mock_assets();
|
||||
let asset = assets.iter().find(|a| a.id == asset_id);
|
||||
|
||||
match asset {
|
||||
Some(asset) => {
|
||||
println!("DEBUG: Found asset with ID {}", asset_id);
|
||||
|
||||
// Convert asset to JSON
|
||||
let asset_json = Self::asset_to_json(asset);
|
||||
|
||||
context.insert("asset", &asset_json);
|
||||
|
||||
// Add valuation history for chart
|
||||
let valuation_history: Vec<serde_json::Map<String, serde_json::Value>> = asset
|
||||
.sorted_valuation_history()
|
||||
.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
|
||||
})
|
||||
.collect();
|
||||
|
||||
context.insert("valuation_history", &valuation_history);
|
||||
|
||||
println!("DEBUG: Rendering asset detail template");
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display the create asset form
|
||||
pub async fn create_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting create asset form rendering");
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
// Add asset types for dropdown
|
||||
let asset_types = vec![
|
||||
("NFT", "NFT"),
|
||||
("Token", "Token"),
|
||||
("RealEstate", "Real Estate"),
|
||||
("Commodity", "Commodity"),
|
||||
("Share", "Share"),
|
||||
("Bond", "Bond"),
|
||||
("IntellectualProperty", "Intellectual Property"),
|
||||
("Other", "Other")
|
||||
];
|
||||
|
||||
context.insert("asset_types", &asset_types);
|
||||
|
||||
println!("DEBUG: Rendering create asset form template");
|
||||
let response = render_template(&tmpl, "assets/create.html", &context);
|
||||
println!("DEBUG: Finished rendering create asset form template");
|
||||
response
|
||||
}
|
||||
|
||||
// Process the create asset form
|
||||
pub async fn create(
|
||||
_tmpl: web::Data<Tera>,
|
||||
_form: web::Form<AssetForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Processing create asset form");
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Add a valuation to an asset
|
||||
pub async fn add_valuation(
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
_form: web::Form<ValuationForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
let asset_id = path.into_inner();
|
||||
|
||||
println!("DEBUG: Adding valuation to asset with ID {}", asset_id);
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Add a transaction to an asset
|
||||
pub async fn add_transaction(
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<String>,
|
||||
_form: web::Form<TransactionForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
let asset_id = path.into_inner();
|
||||
|
||||
println!("DEBUG: Adding transaction to asset with ID {}", asset_id);
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Update the status of an asset
|
||||
pub async fn update_status(
|
||||
_tmpl: web::Data<Tera>,
|
||||
path: web::Path<(String, String)>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (asset_id, _status) = path.into_inner();
|
||||
|
||||
println!("DEBUG: Updating status of asset with ID {}", asset_id);
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Test method to render a simple test page
|
||||
pub async fn test(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
println!("DEBUG: Starting test page rendering");
|
||||
|
||||
let mut context = Context::new();
|
||||
|
||||
let assets = Self::get_mock_assets();
|
||||
println!("DEBUG: Generated {} mock assets for test", assets.len());
|
||||
|
||||
let stats = AssetStatistics::new(&assets);
|
||||
println!("DEBUG: Generated asset statistics for test: {:?}", stats);
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
// Add stats
|
||||
context.insert("stats", &serde_json::to_value(stats).unwrap());
|
||||
println!("DEBUG: Added stats to context for test");
|
||||
|
||||
// Add recent assets
|
||||
let recent_assets: Vec<serde_json::Map<String, serde_json::Value>> = assets
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|a| Self::asset_to_json(a))
|
||||
.collect();
|
||||
|
||||
context.insert("recent_assets", &recent_assets);
|
||||
|
||||
println!("DEBUG: Rendering test_base.html with full context");
|
||||
let response = render_template(&tmpl, "test_base.html", &context);
|
||||
println!("DEBUG: Finished rendering test_base.html");
|
||||
response
|
||||
}
|
||||
|
||||
// Helper method to convert Asset to a JSON object for templates
|
||||
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()));
|
||||
|
||||
// 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()));
|
||||
|
||||
if let Some(valuation_currency) = &asset.valuation_currency {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
// 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()));
|
||||
|
||||
if let Some(transaction_hash) = &blockchain_info.transaction_hash {
|
||||
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)));
|
||||
}
|
||||
|
||||
if let Some(timestamp) = blockchain_info.timestamp {
|
||||
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));
|
||||
}
|
||||
|
||||
// Add valuation history
|
||||
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()));
|
||||
|
||||
if let Some(notes) = &v.notes {
|
||||
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));
|
||||
|
||||
// Add transaction history
|
||||
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()));
|
||||
|
||||
if let Some(from_address) = &t.from_address {
|
||||
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()));
|
||||
}
|
||||
|
||||
if let Some(amount) = t.amount {
|
||||
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()));
|
||||
}
|
||||
|
||||
if let Some(transaction_hash) = &t.transaction_hash {
|
||||
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()));
|
||||
}
|
||||
|
||||
serde_json::Value::Object(transaction_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Generate mock assets for testing
|
||||
fn get_mock_assets() -> Vec<Asset> {
|
||||
let now = Utc::now();
|
||||
let mut assets = Vec::new();
|
||||
|
||||
// Create Tokenized Real Estate asset
|
||||
let mut zanzibar_resort = Asset {
|
||||
id: "asset-zanzibar-resort".to_string(),
|
||||
name: "Zanzibar Coastal Resort".to_string(),
|
||||
description: "A tokenized luxury eco-resort on the eastern coast of Zanzibar with 20 villas and sustainable energy infrastructure".to_string(),
|
||||
asset_type: AssetType::RealEstate,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: "entity-oceanview-holdings".to_string(),
|
||||
owner_name: "OceanView Holdings Ltd.".to_string(),
|
||||
created_at: now - Duration::days(120),
|
||||
updated_at: now - Duration::days(5),
|
||||
blockchain_info: None,
|
||||
current_valuation: Some(3750000.0),
|
||||
valuation_currency: Some("USD".to_string()),
|
||||
valuation_date: Some(now - Duration::days(15)),
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({
|
||||
"location": "East Coast, Zanzibar",
|
||||
"property_size": "5.2 hectares",
|
||||
"buildings": 22,
|
||||
"tokenization_date": (now - Duration::days(120)).to_rfc3339(),
|
||||
"total_tokens": 10000,
|
||||
"token_price": 375.0
|
||||
}),
|
||||
image_url: Some("https://example.com/zanzibar_resort.jpg".to_string()),
|
||||
external_url: Some("https://oceanviewholdings.zaz/resort".to_string()),
|
||||
};
|
||||
|
||||
zanzibar_resort.add_blockchain_info(BlockchainInfo {
|
||||
blockchain: "Ethereum".to_string(),
|
||||
token_id: "ZRESORT".to_string(),
|
||||
contract_address: "0x123456789abcdef123456789abcdef12345678ab".to_string(),
|
||||
owner_address: "0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
|
||||
transaction_hash: Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()),
|
||||
block_number: Some(9876543),
|
||||
timestamp: Some(now - Duration::days(120)),
|
||||
});
|
||||
|
||||
zanzibar_resort.add_valuation(3500000.0, "USD", "ZAZ Property Registry", Some("Initial tokenization valuation".to_string()));
|
||||
zanzibar_resort.add_valuation(3650000.0, "USD", "International Property Appraisers", Some("Independent third-party valuation".to_string()));
|
||||
zanzibar_resort.add_valuation(3750000.0, "USD", "ZAZ Property Registry", Some("Updated valuation after infrastructure improvements".to_string()));
|
||||
|
||||
zanzibar_resort.add_transaction(
|
||||
"Tokenization",
|
||||
None,
|
||||
Some("0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string()),
|
||||
Some(3500000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()),
|
||||
Some("Initial property tokenization under ZAZ Property Registry".to_string()),
|
||||
);
|
||||
|
||||
zanzibar_resort.add_transaction(
|
||||
"Token Sale",
|
||||
Some("0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string()),
|
||||
Some("0x7a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
|
||||
Some(375000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0xdef123456789abcdef123456789abcdef123456789abcdef123456789abcdef".to_string()),
|
||||
Some("Sale of 10% ownership tokens to Zanzibar Investment Collective".to_string()),
|
||||
);
|
||||
|
||||
assets.push(zanzibar_resort);
|
||||
|
||||
// Create ZAZ Governance Token
|
||||
let mut zaz_token = Asset {
|
||||
id: "asset-zaz-governance".to_string(),
|
||||
name: "ZAZ Governance Token".to_string(),
|
||||
description: "Official governance token of the Zanzibar Autonomous Zone, used for voting on proposals and zone-wide decisions".to_string(),
|
||||
asset_type: AssetType::Token,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: "entity-zaz-foundation".to_string(),
|
||||
owner_name: "Zanzibar Autonomous Zone Foundation".to_string(),
|
||||
created_at: now - Duration::days(365),
|
||||
updated_at: now - Duration::days(2),
|
||||
blockchain_info: None,
|
||||
current_valuation: Some(12500000.0),
|
||||
valuation_currency: Some("USD".to_string()),
|
||||
valuation_date: Some(now - Duration::days(3)),
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({
|
||||
"total_supply": 10000000,
|
||||
"circulating_supply": 7500000,
|
||||
"governance_weight": 1.0,
|
||||
"minimum_holding_for_proposals": 10000,
|
||||
"launch_date": (now - Duration::days(365)).to_rfc3339()
|
||||
}),
|
||||
image_url: Some("https://example.com/zaz_token.png".to_string()),
|
||||
external_url: Some("https://governance.zaz/token".to_string()),
|
||||
};
|
||||
|
||||
zaz_token.add_blockchain_info(BlockchainInfo {
|
||||
blockchain: "ThreeFold".to_string(),
|
||||
token_id: "ZAZT".to_string(),
|
||||
contract_address: "0xf6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1".to_string(),
|
||||
owner_address: "0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string(),
|
||||
transaction_hash: None,
|
||||
block_number: None,
|
||||
timestamp: Some(now - Duration::days(365)),
|
||||
});
|
||||
|
||||
zaz_token.add_valuation(8000000.0, "USD", "ZAZ Token Exchange", Some("Initial valuation at launch".to_string()));
|
||||
zaz_token.add_valuation(10500000.0, "USD", "ZAZ Token Exchange", Some("Valuation after successful governance implementation".to_string()));
|
||||
zaz_token.add_valuation(12500000.0, "USD", "ZAZ Token Exchange", Some("Current market valuation".to_string()));
|
||||
|
||||
zaz_token.add_transaction(
|
||||
"Distribution",
|
||||
Some("0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string()),
|
||||
Some("0x9a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
|
||||
Some(1000000.0),
|
||||
Some("ZAZT".to_string()),
|
||||
Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
|
||||
Some("Initial token distribution to founding members".to_string()),
|
||||
);
|
||||
|
||||
zaz_token.add_transaction(
|
||||
"Distribution",
|
||||
Some("0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string()),
|
||||
Some("0x8b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c".to_string()),
|
||||
Some(2500000.0),
|
||||
Some("ZAZT".to_string()),
|
||||
Some("0x234567890abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
|
||||
Some("Public token sale for zone participants".to_string()),
|
||||
);
|
||||
|
||||
assets.push(zaz_token);
|
||||
|
||||
// Create Spice Trade Venture Shares
|
||||
let mut spice_trade_shares = Asset {
|
||||
id: "asset-spice-trade-shares".to_string(),
|
||||
name: "Zanzibar Spice Trade Ventures".to_string(),
|
||||
description: "Equity shares in Zanzibar Spice Trade Ventures, a leading exporter of organic spices from the Zanzibar region".to_string(),
|
||||
asset_type: AssetType::Share,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: "entity-spice-trade".to_string(),
|
||||
owner_name: "Zanzibar Spice Trade Ventures Ltd.".to_string(),
|
||||
created_at: now - Duration::days(180),
|
||||
updated_at: now - Duration::days(7),
|
||||
blockchain_info: None,
|
||||
current_valuation: Some(4250000.0),
|
||||
valuation_currency: Some("USD".to_string()),
|
||||
valuation_date: Some(now - Duration::days(7)),
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({
|
||||
"total_shares": 1000000,
|
||||
"share_type": "Common",
|
||||
"business_sector": "Agriculture & Export",
|
||||
"dividend_yield": 5.2,
|
||||
"last_dividend_date": (now - Duration::days(30)).to_rfc3339(),
|
||||
"incorporation_date": (now - Duration::days(180)).to_rfc3339()
|
||||
}),
|
||||
image_url: Some("https://example.com/spice_trade_logo.png".to_string()),
|
||||
external_url: Some("https://spicetrade.zaz".to_string()),
|
||||
};
|
||||
|
||||
spice_trade_shares.add_blockchain_info(BlockchainInfo {
|
||||
blockchain: "Ethereum".to_string(),
|
||||
token_id: "SPICE".to_string(),
|
||||
contract_address: "0x3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string(),
|
||||
owner_address: "0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string(),
|
||||
transaction_hash: Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()),
|
||||
block_number: Some(7654321),
|
||||
timestamp: Some(now - Duration::days(180)),
|
||||
});
|
||||
|
||||
spice_trade_shares.add_valuation(3800000.0, "USD", "ZAZ Business Registry", Some("Initial company valuation at incorporation".to_string()));
|
||||
spice_trade_shares.add_valuation(4000000.0, "USD", "ZAZ Business Registry", Some("Valuation after first export contracts".to_string()));
|
||||
spice_trade_shares.add_valuation(4250000.0, "USD", "ZAZ Business Registry", Some("Current valuation after expansion to European markets".to_string()));
|
||||
|
||||
spice_trade_shares.add_transaction(
|
||||
"Share Issuance",
|
||||
None,
|
||||
Some("0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
|
||||
Some(3800000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0x789abcdef123456789abcdef123456789abcdef123456789abcdef123456789a".to_string()),
|
||||
Some("Initial share issuance at company formation".to_string()),
|
||||
);
|
||||
|
||||
spice_trade_shares.add_transaction(
|
||||
"Share Transfer",
|
||||
Some("0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
|
||||
Some("0x7b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c".to_string()),
|
||||
Some(950000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0x89abcdef123456789abcdef123456789abcdef123456789abcdef123456789ab".to_string()),
|
||||
Some("Sale of 25% equity to East African Growth Partners".to_string()),
|
||||
);
|
||||
|
||||
assets.push(spice_trade_shares);
|
||||
|
||||
// Create Sustainable Energy Patent
|
||||
let mut tidal_energy_patent = Asset {
|
||||
id: "asset-tidal-energy-patent".to_string(),
|
||||
name: "Zanzibar Tidal Energy System Patent".to_string(),
|
||||
description: "Patent for an innovative tidal energy harvesting system designed specifically for the coastal conditions of Zanzibar".to_string(),
|
||||
asset_type: AssetType::IntellectualProperty,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: "entity-zaz-energy-innovations".to_string(),
|
||||
owner_name: "ZAZ Energy Innovations".to_string(),
|
||||
created_at: now - Duration::days(210),
|
||||
updated_at: now - Duration::days(30),
|
||||
blockchain_info: None,
|
||||
current_valuation: Some(2800000.0),
|
||||
valuation_currency: Some("USD".to_string()),
|
||||
valuation_date: Some(now - Duration::days(30)),
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({
|
||||
"patent_number": "ZAZ-PAT-2024-0142",
|
||||
"filing_date": (now - Duration::days(210)).to_rfc3339(),
|
||||
"grant_date": (now - Duration::days(120)).to_rfc3339(),
|
||||
"patent_type": "Utility",
|
||||
"jurisdiction": "Zanzibar Autonomous Zone",
|
||||
"inventors": ["Dr. Amina Juma", "Eng. Ibrahim Hassan", "Dr. Sarah Mbeki"]
|
||||
}),
|
||||
image_url: Some("https://example.com/tidal_energy_diagram.png".to_string()),
|
||||
external_url: Some("https://patents.zaz/ZAZ-PAT-2024-0142".to_string()),
|
||||
};
|
||||
|
||||
tidal_energy_patent.add_blockchain_info(BlockchainInfo {
|
||||
blockchain: "Polygon".to_string(),
|
||||
token_id: "TIDALIP".to_string(),
|
||||
contract_address: "0x2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3".to_string(),
|
||||
owner_address: "0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string(),
|
||||
transaction_hash: Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()),
|
||||
block_number: Some(5432109),
|
||||
timestamp: Some(now - Duration::days(120)),
|
||||
});
|
||||
|
||||
tidal_energy_patent.add_valuation(1500000.0, "USD", "ZAZ IP Registry", Some("Initial patent valuation upon filing".to_string()));
|
||||
tidal_energy_patent.add_valuation(2200000.0, "USD", "ZAZ IP Registry", Some("Valuation after successful prototype testing".to_string()));
|
||||
tidal_energy_patent.add_valuation(2800000.0, "USD", "ZAZ IP Registry", Some("Current valuation after pilot implementation".to_string()));
|
||||
|
||||
tidal_energy_patent.add_transaction(
|
||||
"Registration",
|
||||
None,
|
||||
Some("0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string()),
|
||||
Some(1500000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0x56789abcdef123456789abcdef123456789abcdef123456789abcdef12345678".to_string()),
|
||||
Some("Initial patent registration and tokenization".to_string()),
|
||||
);
|
||||
|
||||
tidal_energy_patent.add_transaction(
|
||||
"Licensing",
|
||||
Some("0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string()),
|
||||
Some("0x5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a".to_string()),
|
||||
Some(450000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0x6789abcdef123456789abcdef123456789abcdef123456789abcdef123456789".to_string()),
|
||||
Some("Licensing agreement with Coastal Energy Solutions".to_string()),
|
||||
);
|
||||
|
||||
assets.push(tidal_energy_patent);
|
||||
|
||||
// Create Digital Art NFT
|
||||
let mut zanzibar_heritage_nft = Asset {
|
||||
id: "asset-heritage-nft".to_string(),
|
||||
name: "Zanzibar Heritage Collection #1".to_string(),
|
||||
description: "Limited edition digital art NFT showcasing Zanzibar's cultural heritage, created by renowned local artist Fatma Busaidy".to_string(),
|
||||
asset_type: AssetType::NFT,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: "entity-zaz-digital-arts".to_string(),
|
||||
owner_name: "ZAZ Digital Arts Collective".to_string(),
|
||||
created_at: now - Duration::days(90),
|
||||
updated_at: now - Duration::days(10),
|
||||
blockchain_info: None,
|
||||
current_valuation: Some(15000.0),
|
||||
valuation_currency: Some("USD".to_string()),
|
||||
valuation_date: Some(now - Duration::days(10)),
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({
|
||||
"artist": "Fatma Busaidy",
|
||||
"edition": "1 of 10",
|
||||
"medium": "Digital Mixed Media",
|
||||
"dimensions": "4000x3000 px",
|
||||
"creation_date": (now - Duration::days(95)).to_rfc3339(),
|
||||
"authenticity_certificate": "ZAZ-ART-CERT-2024-089"
|
||||
}),
|
||||
image_url: Some("https://example.com/zanzibar_heritage_nft.jpg".to_string()),
|
||||
external_url: Some("https://digitalarts.zaz/collections/heritage/1".to_string()),
|
||||
};
|
||||
|
||||
zanzibar_heritage_nft.add_blockchain_info(BlockchainInfo {
|
||||
blockchain: "Ethereum".to_string(),
|
||||
token_id: "HERITAGE1".to_string(),
|
||||
contract_address: "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d".to_string(),
|
||||
owner_address: "0xb794f5ea0ba39494ce839613fffba74279579268".to_string(),
|
||||
transaction_hash: Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
|
||||
block_number: Some(12345678),
|
||||
timestamp: Some(now - Duration::days(90)),
|
||||
});
|
||||
|
||||
zanzibar_heritage_nft.add_valuation(12000.0, "USD", "ZAZ NFT Marketplace", Some("Initial offering price".to_string()));
|
||||
zanzibar_heritage_nft.add_valuation(13500.0, "USD", "ZAZ NFT Marketplace", Some("Valuation after artist exhibition".to_string()));
|
||||
zanzibar_heritage_nft.add_valuation(15000.0, "USD", "ZAZ NFT Marketplace", Some("Current market valuation".to_string()));
|
||||
|
||||
zanzibar_heritage_nft.add_transaction(
|
||||
"Minting",
|
||||
None,
|
||||
Some("0xb794f5ea0ba39494ce839613fffba74279579268".to_string()),
|
||||
Some(0.0),
|
||||
Some("ETH".to_string()),
|
||||
Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
|
||||
Some("Initial NFT minting by artist".to_string()),
|
||||
);
|
||||
|
||||
zanzibar_heritage_nft.add_transaction(
|
||||
"Sale",
|
||||
Some("0xb794f5ea0ba39494ce839613fffba74279579268".to_string()),
|
||||
Some("0xa1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".to_string()),
|
||||
Some(12000.0),
|
||||
Some("USD".to_string()),
|
||||
Some("0x234567890abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
|
||||
Some("Primary sale to ZAZ Digital Arts Collective".to_string()),
|
||||
);
|
||||
|
||||
assets.push(zanzibar_heritage_nft);
|
||||
|
||||
assets
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use actix_web::{web, HttpResponse, Result, Error};
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, SignerStatus};
|
||||
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus};
|
||||
use crate::utils::render_template;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -24,7 +25,7 @@ pub struct ContractController;
|
||||
|
||||
impl ContractController {
|
||||
// Display the contracts dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
@ -67,7 +68,7 @@ impl ContractController {
|
||||
}
|
||||
|
||||
// Display the list of all contracts
|
||||
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
@ -86,7 +87,7 @@ impl ContractController {
|
||||
}
|
||||
|
||||
// Display the list of user's contracts
|
||||
pub async fn my_contracts(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
pub async fn my_contracts(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
let contracts = Self::get_mock_contracts();
|
||||
@ -104,7 +105,7 @@ impl ContractController {
|
||||
}
|
||||
|
||||
// Display a specific contract
|
||||
pub async fn detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
pub async fn detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse, Error> {
|
||||
let contract_id = path.into_inner();
|
||||
let mut context = Context::new();
|
||||
|
||||
@ -113,26 +114,39 @@ impl ContractController {
|
||||
|
||||
// Find the contract by ID
|
||||
let contracts = Self::get_mock_contracts();
|
||||
let contract = contracts.iter().find(|c| c.id == contract_id);
|
||||
|
||||
match contract {
|
||||
Some(contract) => {
|
||||
// Convert contract to JSON
|
||||
let contract_json = Self::contract_to_json(contract);
|
||||
|
||||
context.insert("contract", &contract_json);
|
||||
context.insert("user_has_signed", &false); // Mock data
|
||||
|
||||
render_template(&tmpl, "contracts/contract_detail.html", &context)
|
||||
},
|
||||
None => {
|
||||
Ok(HttpResponse::NotFound().finish())
|
||||
}
|
||||
}
|
||||
// For demo purposes, if the ID doesn't match exactly, just show the first contract
|
||||
// In a real app, we would return a 404 if the contract is not found
|
||||
let contract = if let Some(found) = contracts.iter().find(|c| c.id == contract_id) {
|
||||
found
|
||||
} else {
|
||||
// For demo, just use the first contract
|
||||
contracts.first().unwrap()
|
||||
};
|
||||
|
||||
// Convert contract to JSON
|
||||
let contract_json = Self::contract_to_json(contract);
|
||||
|
||||
// Add contract to context
|
||||
context.insert("contract", &contract_json);
|
||||
|
||||
// Count signed signers for the template
|
||||
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();
|
||||
context.insert("pending_signers", &pending_signers);
|
||||
|
||||
// For demo purposes, set user_has_signed to false
|
||||
// In a real app, we would check if the current user has already signed
|
||||
context.insert("user_has_signed", &false);
|
||||
|
||||
render_template(&tmpl, "contracts/contract_detail.html", &context)
|
||||
}
|
||||
|
||||
// Display the create contract form
|
||||
pub async fn create_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
pub async fn create_form(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
@ -156,7 +170,7 @@ impl ContractController {
|
||||
pub async fn create(
|
||||
_tmpl: web::Data<Tera>,
|
||||
_form: web::Form<ContractForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// In a real application, we would save the contract to the database
|
||||
// For now, we'll just redirect to the contracts list
|
||||
|
||||
@ -167,14 +181,22 @@ impl ContractController {
|
||||
fn contract_to_json(contract: &Contract) -> serde_json::Map<String, serde_json::Value> {
|
||||
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(format!("{:?}", contract.status)));
|
||||
map.insert("contract_type".to_string(), serde_json::Value::String(format!("{:?}", contract.contract_type)));
|
||||
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("created_by".to_string(), serde_json::Value::String("John Doe".to_string())); // Mock data
|
||||
|
||||
// Organization info
|
||||
if let Some(org) = &contract.organization_id {
|
||||
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()
|
||||
@ -183,10 +205,23 @@ impl ContractController {
|
||||
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(format!("{:?}", s.status)));
|
||||
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()));
|
||||
} 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()));
|
||||
} else if s.status == SignerStatus::Rejected {
|
||||
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()));
|
||||
} else {
|
||||
signer_map.insert("comments".to_string(), serde_json::Value::String("".to_string()));
|
||||
}
|
||||
|
||||
serde_json::Value::Object(signer_map)
|
||||
@ -195,14 +230,14 @@ impl ContractController {
|
||||
|
||||
map.insert("signers".to_string(), serde_json::Value::Array(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)));
|
||||
|
||||
// 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)));
|
||||
|
||||
// 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)));
|
||||
|
||||
// Add revisions
|
||||
let revisions: Vec<serde_json::Value> = contract.revisions.iter()
|
||||
.map(|r| {
|
||||
@ -216,13 +251,63 @@ impl ContractController {
|
||||
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()));
|
||||
} 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()));
|
||||
}
|
||||
|
||||
serde_json::Value::Object(revision_map)
|
||||
})
|
||||
.collect();
|
||||
|
||||
map.insert("revisions".to_string(), serde_json::Value::Array(revisions));
|
||||
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)));
|
||||
|
||||
// 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()));
|
||||
|
||||
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()));
|
||||
} 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()));
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
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()));
|
||||
|
||||
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 {
|
||||
@ -238,95 +323,269 @@ impl ContractController {
|
||||
|
||||
// Generate mock contracts for testing
|
||||
fn get_mock_contracts() -> Vec<Contract> {
|
||||
let now = Utc::now();
|
||||
|
||||
let mut contracts = Vec::new();
|
||||
|
||||
// Contract 1 - Draft
|
||||
let mut contract1 = Contract::new(
|
||||
"Employment Agreement - Marketing Manager".to_string(),
|
||||
"Standard employment contract for the Marketing Manager position".to_string(),
|
||||
ContractType::Employment,
|
||||
"John Doe".to_string(),
|
||||
Some("Acme Corp".to_string())
|
||||
);
|
||||
// Mock contract 1 - Signed Service Agreement
|
||||
let mut contract1 = Contract {
|
||||
id: "contract-001".to_string(),
|
||||
title: "Digital Hub Service Agreement".to_string(),
|
||||
description: "Service agreement for cloud hosting and digital infrastructure services provided by the Zanzibar Digital Hub.".to_string(),
|
||||
status: ContractStatus::Signed,
|
||||
contract_type: ContractType::Service,
|
||||
created_by: "Wei Chen".to_string(),
|
||||
created_at: Utc::now() - Duration::days(30),
|
||||
updated_at: Utc::now() - Duration::days(5),
|
||||
organization_id: Some("Zanzibar Digital Hub".to_string()),
|
||||
effective_date: Some(Utc::now() - Duration::days(5)),
|
||||
expiration_date: Some(Utc::now() + Duration::days(365)),
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 2,
|
||||
};
|
||||
|
||||
contract1.effective_date = Some(now + Duration::days(30));
|
||||
contract1.expiration_date = Some(now + Duration::days(395)); // ~1 year
|
||||
// Add signers to contract 1
|
||||
contract1.signers.push(ContractSigner {
|
||||
id: "signer-001".to_string(),
|
||||
name: "Wei Chen".to_string(),
|
||||
email: "wei.chen@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(5)),
|
||||
comments: Some("Approved as per our discussion.".to_string()),
|
||||
});
|
||||
|
||||
contract1.add_revision(
|
||||
"<p>This is a draft employment contract for the Marketing Manager position.</p>".to_string(),
|
||||
"John Doe".to_string(),
|
||||
Some("Initial draft".to_string())
|
||||
);
|
||||
contract1.signers.push(ContractSigner {
|
||||
id: "signer-002".to_string(),
|
||||
name: "Nala Okafor".to_string(),
|
||||
email: "nala.okafor@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(6)),
|
||||
comments: Some("Terms look good. Happy to proceed.".to_string()),
|
||||
});
|
||||
|
||||
// Contract 2 - Pending Signatures
|
||||
let mut contract2 = Contract::new(
|
||||
"Non-Disclosure Agreement - Vendor XYZ".to_string(),
|
||||
"NDA for our partnership with Vendor XYZ".to_string(),
|
||||
ContractType::NDA,
|
||||
"John Doe".to_string(),
|
||||
Some("Acme Corp".to_string())
|
||||
);
|
||||
// Add revisions to contract 1
|
||||
contract1.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
content: "<h1>Digital Hub Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the undersigned client (\"Client\").</p><h2>1. Services</h2><p>Provider agrees to provide Client with cloud hosting and digital infrastructure services as specified in Appendix A.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Client agrees to pay Provider the fees set forth in Appendix B. All fees are due within thirty (30) days of invoice date.</p><h2>4. Confidentiality</h2><p>Each party agrees to maintain the confidentiality of any proprietary information received from the other party during the term of this Agreement.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(35),
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Initial draft of the service agreement.".to_string()),
|
||||
});
|
||||
|
||||
contract2.effective_date = Some(now);
|
||||
contract2.expiration_date = Some(now + Duration::days(730)); // 2 years
|
||||
contract1.revisions.push(ContractRevision {
|
||||
version: 2,
|
||||
content: "<h1>Digital Hub Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the undersigned client (\"Client\").</p><h2>1. Services</h2><p>Provider agrees to provide Client with cloud hosting and digital infrastructure services as specified in Appendix A.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Client agrees to pay Provider the fees set forth in Appendix B. All fees are due within thirty (30) days of invoice date.</p><h2>4. Confidentiality</h2><p>Each party agrees to maintain the confidentiality of any proprietary information received from the other party during the term of this Agreement.</p><h2>5. Data Protection</h2><p>Provider shall implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk, including encryption of personal data, and shall comply with all applicable data protection laws.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(30),
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Added data protection clause as requested by legal.".to_string()),
|
||||
});
|
||||
|
||||
contract2.add_revision(
|
||||
"<p>This is the first version of the NDA with Vendor XYZ.</p>".to_string(),
|
||||
"John Doe".to_string(),
|
||||
Some("Initial draft".to_string())
|
||||
);
|
||||
// Mock contract 2 - Pending Signatures
|
||||
let mut contract2 = Contract {
|
||||
id: "contract-002".to_string(),
|
||||
title: "Software Development Agreement".to_string(),
|
||||
description: "Agreement for custom software development services for the Zanzibar Digital Marketplace platform.".to_string(),
|
||||
status: ContractStatus::PendingSignatures,
|
||||
contract_type: ContractType::SLA,
|
||||
created_by: "Dr. Raj Patel".to_string(),
|
||||
created_at: Utc::now() - Duration::days(10),
|
||||
updated_at: Utc::now() - Duration::days(2),
|
||||
organization_id: Some("Global Tech Solutions".to_string()),
|
||||
effective_date: None,
|
||||
expiration_date: None,
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 1,
|
||||
};
|
||||
|
||||
contract2.add_revision(
|
||||
"<p>This is the revised version of the NDA with Vendor XYZ.</p>".to_string(),
|
||||
"John Doe".to_string(),
|
||||
Some("Added confidentiality period".to_string())
|
||||
);
|
||||
// Add signers to contract 2
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-003".to_string(),
|
||||
name: "Dr. Raj Patel".to_string(),
|
||||
email: "raj.patel@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(2)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
contract2.add_signer("Jane Smith".to_string(), "jane@example.com".to_string());
|
||||
contract2.add_signer("Bob Johnson".to_string(), "bob@vendorxyz.com".to_string());
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-004".to_string(),
|
||||
name: "Maya Rodriguez".to_string(),
|
||||
email: "maya.rodriguez@example.com".to_string(),
|
||||
status: SignerStatus::Pending,
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
// Mark Jane as signed
|
||||
if let Some(signer) = contract2.signers.iter_mut().next() {
|
||||
signer.sign(None);
|
||||
}
|
||||
contract2.signers.push(ContractSigner {
|
||||
id: "signer-005".to_string(),
|
||||
name: "Jamal Washington".to_string(),
|
||||
email: "jamal.washington@example.com".to_string(),
|
||||
status: SignerStatus::Pending,
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
// Send for signatures
|
||||
let _ = contract2.send_for_signatures();
|
||||
// Add revisions to contract 2
|
||||
contract2.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
content: "<h1>Software Development Agreement</h1><p>This Software Development Agreement (the \"Agreement\") is entered into between Global Tech Solutions (\"Developer\") and Zanzibar Digital Hub (\"Client\").</p><h2>1. Scope of Work</h2><p>Developer agrees to design, develop, and implement a digital marketplace platform as specified in the attached Statement of Work.</p><h2>2. Timeline</h2><p>Developer shall complete the development according to the timeline set forth in Appendix A.</p><h2>3. Compensation</h2><p>Client agrees to pay Developer the fees set forth in Appendix B according to the payment schedule therein.</p><h2>4. Intellectual Property</h2><p>Upon full payment, Client shall own all rights, title, and interest in the developed software.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(10),
|
||||
created_by: "Dr. Raj Patel".to_string(),
|
||||
comments: Some("Initial draft of the development agreement.".to_string()),
|
||||
});
|
||||
|
||||
// Contract 3 - Signed
|
||||
let mut contract3 = Contract::new(
|
||||
"Service Agreement - Website Maintenance".to_string(),
|
||||
"Agreement for ongoing website maintenance services".to_string(),
|
||||
ContractType::Service,
|
||||
"John Doe".to_string(),
|
||||
Some("Acme Corp".to_string())
|
||||
);
|
||||
// Mock contract 3 - Draft
|
||||
let mut contract3 = Contract {
|
||||
id: "contract-003".to_string(),
|
||||
title: "Digital Asset Tokenization Agreement".to_string(),
|
||||
description: "Framework agreement for tokenizing real estate assets on the Zanzibar blockchain network.".to_string(),
|
||||
status: ContractStatus::Draft,
|
||||
contract_type: ContractType::Partnership,
|
||||
created_by: "Nala Okafor".to_string(),
|
||||
created_at: Utc::now() - Duration::days(3),
|
||||
updated_at: Utc::now() - Duration::days(1),
|
||||
organization_id: Some("Zanzibar Property Consortium".to_string()),
|
||||
effective_date: None,
|
||||
expiration_date: None,
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 1,
|
||||
};
|
||||
|
||||
contract3.effective_date = Some(now - Duration::days(7));
|
||||
contract3.expiration_date = Some(now + Duration::days(358)); // ~1 year from effective date
|
||||
// Add potential signers to contract 3 (still in draft)
|
||||
contract3.signers.push(ContractSigner {
|
||||
id: "signer-006".to_string(),
|
||||
name: "Nala Okafor".to_string(),
|
||||
email: "nala.okafor@example.com".to_string(),
|
||||
status: SignerStatus::Pending,
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
contract3.add_revision(
|
||||
"<p>This is a service agreement for website maintenance.</p>".to_string(),
|
||||
"John Doe".to_string(),
|
||||
Some("Initial version".to_string())
|
||||
);
|
||||
contract3.signers.push(ContractSigner {
|
||||
id: "signer-007".to_string(),
|
||||
name: "Ibrahim Al-Farsi".to_string(),
|
||||
email: "ibrahim.alfarsi@example.com".to_string(),
|
||||
status: SignerStatus::Pending,
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
});
|
||||
|
||||
contract3.add_signer("Jane Smith".to_string(), "jane@example.com".to_string());
|
||||
contract3.add_signer("Alice Brown".to_string(), "alice@webmaintenance.com".to_string());
|
||||
// Add revisions to contract 3
|
||||
contract3.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
content: "<h1>Digital Asset Tokenization Agreement</h1><p>This Digital Asset Tokenization Agreement (the \"Agreement\") is entered into between Zanzibar Property Consortium (\"Tokenizer\") and the property owners listed in Appendix A (\"Owners\").</p><h2>1. Purpose</h2><p>The purpose of this Agreement is to establish the terms and conditions for tokenizing real estate assets on the Zanzibar blockchain network.</p><h2>2. Tokenization Process</h2><p>Tokenizer shall create digital tokens representing ownership interests in the properties listed in Appendix A according to the specifications in Appendix B.</p><h2>3. Revenue Sharing</h2><p>Revenue generated from the tokenized properties shall be distributed according to the formula set forth in Appendix C.</p><h2>4. Governance</h2><p>Decisions regarding the management of tokenized properties shall be made according to the governance framework outlined in Appendix D.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(3),
|
||||
created_by: "Nala Okafor".to_string(),
|
||||
comments: Some("Initial draft of the tokenization agreement.".to_string()),
|
||||
});
|
||||
|
||||
// Mark both signers as signed
|
||||
for signer in contract3.signers.iter_mut() {
|
||||
signer.sign(None);
|
||||
}
|
||||
// Mock contract 4 - Rejected
|
||||
let mut contract4 = Contract {
|
||||
id: "contract-004".to_string(),
|
||||
title: "Data Sharing Agreement".to_string(),
|
||||
description: "Agreement governing the sharing of anonymized data between Zanzibar Digital Hub and research institutions.".to_string(),
|
||||
status: ContractStatus::Draft,
|
||||
contract_type: ContractType::NDA,
|
||||
created_by: "Wei Chen".to_string(),
|
||||
created_at: Utc::now() - Duration::days(15),
|
||||
updated_at: Utc::now() - Duration::days(8),
|
||||
organization_id: Some("Zanzibar Digital Hub".to_string()),
|
||||
effective_date: None,
|
||||
expiration_date: None,
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 1,
|
||||
};
|
||||
|
||||
// Mark as signed
|
||||
contract3.status = ContractStatus::Signed;
|
||||
// Add signers to contract 4 with a rejection
|
||||
contract4.signers.push(ContractSigner {
|
||||
id: "signer-008".to_string(),
|
||||
name: "Wei Chen".to_string(),
|
||||
email: "wei.chen@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(10)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
contract4.signers.push(ContractSigner {
|
||||
id: "signer-009".to_string(),
|
||||
name: "Dr. Amina Diallo".to_string(),
|
||||
email: "amina.diallo@example.com".to_string(),
|
||||
status: SignerStatus::Rejected,
|
||||
signed_at: Some(Utc::now() - Duration::days(8)),
|
||||
comments: Some("Cannot agree to these terms due to privacy concerns. Please revise section 3.2 regarding data retention.".to_string()),
|
||||
});
|
||||
|
||||
// Add revisions to contract 4
|
||||
contract4.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
content: "<h1>Data Sharing Agreement</h1><p>This Data Sharing Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the research institutions listed in Appendix A (\"Recipients\").</p><h2>1. Purpose</h2><p>The purpose of this Agreement is to establish the terms and conditions for sharing anonymized data for research purposes.</p><h2>2. Data Description</h2><p>Provider shall share the data described in Appendix B, which shall be anonymized according to the protocol in Appendix C.</p><h2>3. Data Use</h2><p>Recipients may use the shared data solely for the research purposes described in Appendix D.</p><h2>3.1 Publication</h2><p>Recipients may publish research findings based on the shared data, provided that they acknowledge Provider as the data source.</p><h2>3.2 Data Retention</h2><p>Recipients shall retain the shared data for a period of five (5) years, after which they shall securely delete all copies.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(15),
|
||||
created_by: "Wei Chen".to_string(),
|
||||
comments: Some("Initial draft of the data sharing agreement.".to_string()),
|
||||
});
|
||||
|
||||
// Mock contract 5 - Active
|
||||
let mut contract5 = Contract {
|
||||
id: "contract-005".to_string(),
|
||||
title: "Digital Identity Verification Service Agreement".to_string(),
|
||||
description: "Agreement for providing digital identity verification services to businesses operating in the Zanzibar Autonomous Zone.".to_string(),
|
||||
status: ContractStatus::Active,
|
||||
contract_type: ContractType::Service,
|
||||
created_by: "Maya Rodriguez".to_string(),
|
||||
created_at: Utc::now() - Duration::days(60),
|
||||
updated_at: Utc::now() - Duration::days(45),
|
||||
organization_id: Some("Zanzibar Digital Hub".to_string()),
|
||||
effective_date: Some(Utc::now() - Duration::days(45)),
|
||||
expiration_date: Some(Utc::now() + Duration::days(305)),
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 2,
|
||||
};
|
||||
|
||||
// Add signers to contract 5
|
||||
contract5.signers.push(ContractSigner {
|
||||
id: "signer-010".to_string(),
|
||||
name: "Maya Rodriguez".to_string(),
|
||||
email: "maya.rodriguez@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(47)),
|
||||
comments: None,
|
||||
});
|
||||
|
||||
contract5.signers.push(ContractSigner {
|
||||
id: "signer-011".to_string(),
|
||||
name: "Li Wei".to_string(),
|
||||
email: "li.wei@example.com".to_string(),
|
||||
status: SignerStatus::Signed,
|
||||
signed_at: Some(Utc::now() - Duration::days(45)),
|
||||
comments: Some("Approved after legal review.".to_string()),
|
||||
});
|
||||
|
||||
// Add revisions to contract 5
|
||||
contract5.revisions.push(ContractRevision {
|
||||
version: 1,
|
||||
content: "<h1>Digital Identity Verification Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the businesses listed in Appendix A (\"Clients\").</p><h2>1. Services</h2><p>Provider agrees to provide Clients with digital identity verification services as specified in Appendix B.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Clients agree to pay Provider the fees set forth in Appendix C. All fees are due within thirty (30) days of invoice date.</p><h2>4. Service Level Agreement</h2><p>Provider shall maintain a service uptime of at least 99.9% as measured on a monthly basis.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(60),
|
||||
created_by: "Maya Rodriguez".to_string(),
|
||||
comments: Some("Initial draft of the identity verification service agreement.".to_string()),
|
||||
});
|
||||
|
||||
contract5.revisions.push(ContractRevision {
|
||||
version: 2,
|
||||
content: "<h1>Digital Identity Verification Service Agreement</h1><p>This Service Agreement (the \"Agreement\") is entered into between Zanzibar Digital Hub (\"Provider\") and the businesses listed in Appendix A (\"Clients\").</p><h2>1. Services</h2><p>Provider agrees to provide Clients with digital identity verification services as specified in Appendix B.</p><h2>2. Term</h2><p>This Agreement shall commence on the Effective Date and continue for a period of one (1) year unless terminated earlier in accordance with the terms herein.</p><h2>3. Fees</h2><p>Clients agree to pay Provider the fees set forth in Appendix C. All fees are due within thirty (30) days of invoice date.</p><h2>4. Service Level Agreement</h2><p>Provider shall maintain a service uptime of at least 99.9% as measured on a monthly basis.</p><h2>5. Compliance</h2><p>Provider shall comply with all applicable laws and regulations regarding identity verification and data protection, including but not limited to the Zanzibar Digital Economy Act.</p>".to_string(),
|
||||
created_at: Utc::now() - Duration::days(50),
|
||||
created_by: "Maya Rodriguez".to_string(),
|
||||
comments: Some("Added compliance clause as requested by legal.".to_string()),
|
||||
});
|
||||
|
||||
// Add all contracts to the vector
|
||||
contracts.push(contract1);
|
||||
contracts.push(contract2);
|
||||
contracts.push(contract3);
|
||||
contracts.push(contract4);
|
||||
contracts.push(contract5);
|
||||
|
||||
contracts
|
||||
}
|
||||
|
@ -187,226 +187,421 @@ impl FlowController {
|
||||
// Create a few mock flows
|
||||
let mut flow1 = Flow {
|
||||
id: "flow-1".to_string(),
|
||||
name: "Deploy Website".to_string(),
|
||||
description: "Deploy a new website to production".to_string(),
|
||||
flow_type: FlowType::ServiceActivation,
|
||||
name: "ZAZ Business Entity Registration".to_string(),
|
||||
description: "Register a new business entity within the Zanzibar Autonomous Zone legal framework".to_string(),
|
||||
flow_type: FlowType::CompanyRegistration,
|
||||
status: FlowStatus::InProgress,
|
||||
owner_id: "user-1".to_string(),
|
||||
owner_name: "John Doe".to_string(),
|
||||
owner_name: "Ibrahim Faraji".to_string(),
|
||||
steps: vec![
|
||||
FlowStep {
|
||||
id: "step-1-1".to_string(),
|
||||
name: "Build".to_string(),
|
||||
description: "Build the website".to_string(),
|
||||
name: "Document Submission".to_string(),
|
||||
description: "Submit required business registration documents including business plan, ownership structure, and KYC information".to_string(),
|
||||
order: 1,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(1)),
|
||||
completed_at: Some(Utc::now() - Duration::hours(23)),
|
||||
started_at: Some(Utc::now() - Duration::days(5)),
|
||||
completed_at: Some(Utc::now() - Duration::days(4)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-1-1-1".to_string(),
|
||||
message: "Build started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(1),
|
||||
message: "Initial document package submitted".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(5),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-1-1-2".to_string(),
|
||||
message: "Build completed successfully".to_string(),
|
||||
timestamp: Utc::now() - Duration::hours(23),
|
||||
message: "Additional ownership verification documents requested".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(4) - Duration::hours(12),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-1-1-3".to_string(),
|
||||
message: "Additional documents submitted and verified".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(4),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-1-2".to_string(),
|
||||
name: "Test".to_string(),
|
||||
description: "Run tests on the website".to_string(),
|
||||
name: "Regulatory Review".to_string(),
|
||||
description: "ZAZ Business Registry review of submitted documents and compliance with regulatory requirements".to_string(),
|
||||
order: 2,
|
||||
status: StepStatus::InProgress,
|
||||
started_at: Some(Utc::now() - Duration::hours(22)),
|
||||
started_at: Some(Utc::now() - Duration::days(3)),
|
||||
completed_at: None,
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-1-2-1".to_string(),
|
||||
message: "Tests started".to_string(),
|
||||
timestamp: Utc::now() - Duration::hours(22),
|
||||
message: "Regulatory review initiated by ZAZ Business Registry".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-1-2-2".to_string(),
|
||||
message: "Preliminary compliance assessment completed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(2),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-1-2-3".to_string(),
|
||||
message: "Awaiting final approval from regulatory committee".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(1),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-1-3".to_string(),
|
||||
name: "Deploy".to_string(),
|
||||
description: "Deploy the website to production".to_string(),
|
||||
name: "Digital Identity Creation".to_string(),
|
||||
description: "Creation of the entity's digital identity and blockchain credentials within the ZAZ ecosystem".to_string(),
|
||||
order: 3,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
logs: vec![],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-1-4".to_string(),
|
||||
name: "License and Certificate Issuance".to_string(),
|
||||
description: "Issuance of business licenses, certificates, and digital credentials".to_string(),
|
||||
order: 4,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
logs: vec![],
|
||||
},
|
||||
],
|
||||
created_at: Utc::now() - Duration::days(1),
|
||||
updated_at: Utc::now() - Duration::hours(22),
|
||||
created_at: Utc::now() - Duration::days(5),
|
||||
updated_at: Utc::now() - Duration::days(1),
|
||||
completed_at: None,
|
||||
progress_percentage: 33,
|
||||
progress_percentage: 40,
|
||||
current_step: None,
|
||||
};
|
||||
|
||||
// Update the current step
|
||||
flow1.current_step = flow1.steps.iter().find(|s| s.status == StepStatus::InProgress).cloned();
|
||||
|
||||
let flow2 = Flow {
|
||||
let mut flow2 = Flow {
|
||||
id: "flow-2".to_string(),
|
||||
name: "Database Migration".to_string(),
|
||||
description: "Migrate database to new schema".to_string(),
|
||||
flow_type: FlowType::CompanyRegistration,
|
||||
name: "Digital Asset Tokenization Approval".to_string(),
|
||||
description: "Process for approving the tokenization of a real estate asset within the ZAZ regulatory framework".to_string(),
|
||||
flow_type: FlowType::AssetTokenization,
|
||||
status: FlowStatus::Completed,
|
||||
owner_id: "user-2".to_string(),
|
||||
owner_name: "Jane Smith".to_string(),
|
||||
owner_name: "Amina Salim".to_string(),
|
||||
steps: vec![
|
||||
FlowStep {
|
||||
id: "step-2-1".to_string(),
|
||||
name: "Backup".to_string(),
|
||||
description: "Backup the database".to_string(),
|
||||
name: "Asset Verification".to_string(),
|
||||
description: "Verification of the underlying asset ownership and valuation".to_string(),
|
||||
order: 1,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(3)),
|
||||
completed_at: Some(Utc::now() - Duration::days(3) + Duration::hours(2)),
|
||||
started_at: Some(Utc::now() - Duration::days(30)),
|
||||
completed_at: Some(Utc::now() - Duration::days(25)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-2-1-1".to_string(),
|
||||
message: "Backup started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3),
|
||||
message: "Asset documentation submitted for verification".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(30),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-1-2".to_string(),
|
||||
message: "Backup completed successfully".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3) + Duration::hours(2),
|
||||
message: "Independent valuation completed by ZAZ Property Registry".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(27),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-1-3".to_string(),
|
||||
message: "Asset ownership and valuation verified".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(25),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-2-2".to_string(),
|
||||
name: "Migrate".to_string(),
|
||||
description: "Run migration scripts".to_string(),
|
||||
name: "Tokenization Structure Review".to_string(),
|
||||
description: "Review of the proposed token structure, distribution model, and compliance with ZAZ tokenization standards".to_string(),
|
||||
order: 2,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(3) + Duration::hours(3)),
|
||||
completed_at: Some(Utc::now() - Duration::days(3) + Duration::hours(5)),
|
||||
started_at: Some(Utc::now() - Duration::days(24)),
|
||||
completed_at: Some(Utc::now() - Duration::days(20)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-2-2-1".to_string(),
|
||||
message: "Migration started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3) + Duration::hours(3),
|
||||
message: "Tokenization proposal submitted for review".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(24),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-2-2".to_string(),
|
||||
message: "Migration completed successfully".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3) + Duration::hours(5),
|
||||
message: "Technical review completed by ZAZ Digital Assets Committee".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(22),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-2-3".to_string(),
|
||||
message: "Tokenization structure approved with minor modifications".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(20),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-2-3".to_string(),
|
||||
name: "Verify".to_string(),
|
||||
description: "Verify the migration".to_string(),
|
||||
name: "Smart Contract Deployment".to_string(),
|
||||
description: "Deployment and verification of the asset tokenization smart contracts".to_string(),
|
||||
order: 3,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(3) + Duration::hours(6)),
|
||||
completed_at: Some(Utc::now() - Duration::days(3) + Duration::hours(7)),
|
||||
started_at: Some(Utc::now() - Duration::days(19)),
|
||||
completed_at: Some(Utc::now() - Duration::days(15)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-2-3-1".to_string(),
|
||||
message: "Verification started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3) + Duration::hours(6),
|
||||
message: "Smart contract code submitted for audit".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(19),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-3-2".to_string(),
|
||||
message: "Verification completed successfully".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3) + Duration::hours(7),
|
||||
message: "Security audit completed with no critical issues".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(17),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-3-3".to_string(),
|
||||
message: "Smart contracts deployed to ZAZ-approved blockchain".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(15),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-2-4".to_string(),
|
||||
name: "Final Approval and Listing".to_string(),
|
||||
description: "Final regulatory approval and listing on the ZAZ Digital Asset Exchange".to_string(),
|
||||
order: 4,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(14)),
|
||||
completed_at: Some(Utc::now() - Duration::days(10)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-2-4-1".to_string(),
|
||||
message: "Final documentation package submitted for approval".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(14),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-4-2".to_string(),
|
||||
message: "Regulatory approval granted by ZAZ Financial Authority".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(12),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-2-4-3".to_string(),
|
||||
message: "Asset tokens listed on ZAZ Digital Asset Exchange".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(10),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
created_at: Utc::now() - Duration::days(3),
|
||||
updated_at: Utc::now() - Duration::days(3) + Duration::hours(7),
|
||||
completed_at: Some(Utc::now() - Duration::days(3) + Duration::hours(7)),
|
||||
created_at: Utc::now() - Duration::days(30),
|
||||
updated_at: Utc::now() - Duration::days(10),
|
||||
completed_at: Some(Utc::now() - Duration::days(10)),
|
||||
progress_percentage: 100,
|
||||
current_step: None,
|
||||
};
|
||||
|
||||
flow2.current_step = flow2.steps.last().cloned();
|
||||
|
||||
let mut flow3 = Flow {
|
||||
id: "flow-3".to_string(),
|
||||
name: "Server Maintenance".to_string(),
|
||||
description: "Perform server maintenance".to_string(),
|
||||
flow_type: FlowType::PaymentProcessing,
|
||||
name: "Sustainable Tourism Certification".to_string(),
|
||||
description: "Application process for ZAZ Sustainable Tourism Certification for eco-tourism businesses".to_string(),
|
||||
flow_type: FlowType::Certification,
|
||||
status: FlowStatus::Stuck,
|
||||
owner_id: "user-1".to_string(),
|
||||
owner_name: "John Doe".to_string(),
|
||||
owner_id: "user-3".to_string(),
|
||||
owner_name: "Hassan Mwinyi".to_string(),
|
||||
steps: vec![
|
||||
FlowStep {
|
||||
id: "step-3-1".to_string(),
|
||||
name: "Backup".to_string(),
|
||||
description: "Backup the server".to_string(),
|
||||
name: "Initial Application".to_string(),
|
||||
description: "Submission of initial application and supporting documentation".to_string(),
|
||||
order: 1,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(2)),
|
||||
completed_at: Some(Utc::now() - Duration::days(2) + Duration::hours(1)),
|
||||
started_at: Some(Utc::now() - Duration::days(15)),
|
||||
completed_at: Some(Utc::now() - Duration::days(12)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-3-1-1".to_string(),
|
||||
message: "Backup started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(2),
|
||||
message: "Application submitted for Coral Reef Eco Tours".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(15),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-3-1-2".to_string(),
|
||||
message: "Backup completed successfully".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(2) + Duration::hours(1),
|
||||
message: "Application fee payment confirmed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(14),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-3-1-3".to_string(),
|
||||
message: "Initial documentation review completed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(12),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-3-2".to_string(),
|
||||
name: "Update".to_string(),
|
||||
description: "Update the server".to_string(),
|
||||
name: "Environmental Impact Assessment".to_string(),
|
||||
description: "Assessment of the business's environmental impact and sustainability practices".to_string(),
|
||||
order: 2,
|
||||
status: StepStatus::Stuck,
|
||||
started_at: Some(Utc::now() - Duration::days(2) + Duration::hours(2)),
|
||||
started_at: Some(Utc::now() - Duration::days(11)),
|
||||
completed_at: None,
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-3-2-1".to_string(),
|
||||
message: "Update started".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(2) + Duration::hours(2),
|
||||
message: "Environmental assessment initiated".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(11),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-3-2-2".to_string(),
|
||||
message: "Update failed: Disk space issue".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(2) + Duration::hours(3),
|
||||
message: "Site visit scheduled with environmental officer".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(9),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-3-2-3".to_string(),
|
||||
message: "STUCK: Missing required marine conservation plan documentation".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(7),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-3-3".to_string(),
|
||||
name: "Restart".to_string(),
|
||||
description: "Restart the server".to_string(),
|
||||
name: "Community Engagement Verification".to_string(),
|
||||
description: "Verification of community engagement and benefit-sharing mechanisms".to_string(),
|
||||
order: 3,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
logs: vec![],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-3-4".to_string(),
|
||||
name: "Certification Issuance".to_string(),
|
||||
description: "Final review and issuance of ZAZ Sustainable Tourism Certification".to_string(),
|
||||
order: 4,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
logs: vec![],
|
||||
},
|
||||
],
|
||||
created_at: Utc::now() - Duration::days(2),
|
||||
updated_at: Utc::now() - Duration::days(2) + Duration::hours(3),
|
||||
created_at: Utc::now() - Duration::days(15),
|
||||
updated_at: Utc::now() - Duration::days(7),
|
||||
completed_at: None,
|
||||
progress_percentage: 33,
|
||||
progress_percentage: 35,
|
||||
current_step: None,
|
||||
};
|
||||
|
||||
// Update the current step
|
||||
flow3.current_step = flow3.steps.iter().find(|s| s.status == StepStatus::Stuck).cloned();
|
||||
|
||||
let mut flow4 = Flow {
|
||||
id: "flow-4".to_string(),
|
||||
name: "Digital Payment Provider License".to_string(),
|
||||
description: "Application for a license to operate as a digital payment provider within the ZAZ financial system".to_string(),
|
||||
flow_type: FlowType::LicenseApplication,
|
||||
status: FlowStatus::InProgress,
|
||||
owner_id: "user-4".to_string(),
|
||||
owner_name: "Fatma Busaidy".to_string(),
|
||||
steps: vec![
|
||||
FlowStep {
|
||||
id: "step-4-1".to_string(),
|
||||
name: "Initial Application".to_string(),
|
||||
description: "Submission of license application and company information".to_string(),
|
||||
order: 1,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(20)),
|
||||
completed_at: Some(Utc::now() - Duration::days(18)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-4-1-1".to_string(),
|
||||
message: "Application submitted for ZanziPay digital payment services".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(20),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-1-2".to_string(),
|
||||
message: "Application fee payment confirmed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(19),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-1-3".to_string(),
|
||||
message: "Initial documentation review completed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(18),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-4-2".to_string(),
|
||||
name: "Technical Infrastructure Review".to_string(),
|
||||
description: "Review of the technical infrastructure, security measures, and compliance with ZAZ financial standards".to_string(),
|
||||
order: 2,
|
||||
status: StepStatus::Completed,
|
||||
started_at: Some(Utc::now() - Duration::days(17)),
|
||||
completed_at: Some(Utc::now() - Duration::days(10)),
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-4-2-1".to_string(),
|
||||
message: "Technical documentation submitted for review".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(17),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-2-2".to_string(),
|
||||
message: "Security audit initiated by ZAZ Financial Technology Office".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(15),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-2-3".to_string(),
|
||||
message: "Technical infrastructure approved with recommendations".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(10),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-4-3".to_string(),
|
||||
name: "AML/KYC Compliance Review".to_string(),
|
||||
description: "Review of anti-money laundering and know-your-customer procedures".to_string(),
|
||||
order: 3,
|
||||
status: StepStatus::InProgress,
|
||||
started_at: Some(Utc::now() - Duration::days(9)),
|
||||
completed_at: None,
|
||||
logs: vec![
|
||||
FlowLog {
|
||||
id: "log-4-3-1".to_string(),
|
||||
message: "AML/KYC documentation submitted for review".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(9),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-3-2".to_string(),
|
||||
message: "Initial compliance assessment completed".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(5),
|
||||
},
|
||||
FlowLog {
|
||||
id: "log-4-3-3".to_string(),
|
||||
message: "Additional KYC procedure documentation requested".to_string(),
|
||||
timestamp: Utc::now() - Duration::days(3),
|
||||
},
|
||||
],
|
||||
},
|
||||
FlowStep {
|
||||
id: "step-4-4".to_string(),
|
||||
name: "License Issuance".to_string(),
|
||||
description: "Final review and issuance of Digital Payment Provider License".to_string(),
|
||||
order: 4,
|
||||
status: StepStatus::Pending,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
logs: vec![],
|
||||
},
|
||||
],
|
||||
created_at: Utc::now() - Duration::days(20),
|
||||
updated_at: Utc::now() - Duration::days(3),
|
||||
completed_at: None,
|
||||
progress_percentage: 65,
|
||||
current_step: None,
|
||||
};
|
||||
|
||||
flow4.current_step = flow4.steps.iter().find(|s| s.status == StepStatus::InProgress).cloned();
|
||||
|
||||
flows.push(flow1);
|
||||
flows.push(flow2);
|
||||
flows.push(flow3);
|
||||
flows.push(flow4);
|
||||
|
||||
flows
|
||||
}
|
||||
|
@ -228,9 +228,9 @@ impl GovernanceController {
|
||||
Proposal {
|
||||
id: "prop-001".to_string(),
|
||||
creator_id: 1,
|
||||
creator_name: "John Doe".to_string(),
|
||||
title: "Implement Community Rewards Program".to_string(),
|
||||
description: "This proposal aims to implement a community rewards program to incentivize active participation in the platform.".to_string(),
|
||||
creator_name: "Ibrahim Faraji".to_string(),
|
||||
title: "Establish Zanzibar Digital Trade Hub".to_string(),
|
||||
description: "This proposal aims to create a dedicated digital trade hub within the Zanzibar Autonomous Zone to facilitate international e-commerce for local businesses. The hub will provide logistics support, digital marketing services, and regulatory compliance assistance to help Zanzibar businesses reach global markets.".to_string(),
|
||||
status: ProposalStatus::Active,
|
||||
created_at: now - Duration::days(5),
|
||||
updated_at: now - Duration::days(5),
|
||||
@ -240,9 +240,9 @@ impl GovernanceController {
|
||||
Proposal {
|
||||
id: "prop-002".to_string(),
|
||||
creator_id: 2,
|
||||
creator_name: "Jane Smith".to_string(),
|
||||
title: "Platform UI Redesign".to_string(),
|
||||
description: "A comprehensive redesign of the platform's user interface to improve usability and accessibility.".to_string(),
|
||||
creator_name: "Amina Salim".to_string(),
|
||||
title: "ZAZ Sustainable Tourism Framework".to_string(),
|
||||
description: "A comprehensive framework for sustainable tourism development within the Zanzibar Autonomous Zone. This proposal outlines environmental standards, community benefit-sharing mechanisms, and digital infrastructure for eco-tourism businesses. It includes tokenization standards for tourism assets and a certification system for sustainable operators.".to_string(),
|
||||
status: ProposalStatus::Approved,
|
||||
created_at: now - Duration::days(15),
|
||||
updated_at: now - Duration::days(2),
|
||||
@ -252,9 +252,9 @@ impl GovernanceController {
|
||||
Proposal {
|
||||
id: "prop-003".to_string(),
|
||||
creator_id: 3,
|
||||
creator_name: "Bob Johnson".to_string(),
|
||||
title: "Add Support for Mobile Notifications".to_string(),
|
||||
description: "Implement push notifications for mobile users to improve engagement and user experience.".to_string(),
|
||||
creator_name: "Hassan Mwinyi".to_string(),
|
||||
title: "Spice Industry Modernization Initiative".to_string(),
|
||||
description: "This proposal seeks to modernize Zanzibar's traditional spice industry through blockchain-based supply chain tracking, international quality certification, and digital marketplace integration. The initiative will help local spice farmers and processors access premium international markets while preserving traditional cultivation methods.".to_string(),
|
||||
status: ProposalStatus::Draft,
|
||||
created_at: now - Duration::days(1),
|
||||
updated_at: now - Duration::days(1),
|
||||
@ -264,9 +264,9 @@ impl GovernanceController {
|
||||
Proposal {
|
||||
id: "prop-004".to_string(),
|
||||
creator_id: 1,
|
||||
creator_name: "John Doe".to_string(),
|
||||
title: "Integrate with Third-party Payment Providers".to_string(),
|
||||
description: "Add support for additional payment providers to give users more options for transactions.".to_string(),
|
||||
creator_name: "Ibrahim Faraji".to_string(),
|
||||
title: "ZAZ Regulatory Framework for Digital Financial Services".to_string(),
|
||||
description: "Establish a comprehensive regulatory framework for digital financial services within the Zanzibar Autonomous Zone. This includes licensing requirements for crypto exchanges, digital payment providers, and tokenized asset platforms operating within the zone, while ensuring compliance with international AML/KYC standards.".to_string(),
|
||||
status: ProposalStatus::Rejected,
|
||||
created_at: now - Duration::days(20),
|
||||
updated_at: now - Duration::days(5),
|
||||
@ -276,15 +276,39 @@ impl GovernanceController {
|
||||
Proposal {
|
||||
id: "prop-005".to_string(),
|
||||
creator_id: 4,
|
||||
creator_name: "Alice Williams".to_string(),
|
||||
title: "Implement Two-Factor Authentication".to_string(),
|
||||
description: "Enhance security by implementing two-factor authentication for all user accounts.".to_string(),
|
||||
creator_name: "Fatma Busaidy".to_string(),
|
||||
title: "Digital Arts Incubator and NFT Marketplace".to_string(),
|
||||
description: "Create a dedicated digital arts incubator and NFT marketplace to support Zanzibar's creative economy. The initiative will provide technical training, equipment, and a curated marketplace for local artists to create and sell digital art that celebrates Zanzibar's rich cultural heritage while accessing global markets.".to_string(),
|
||||
status: ProposalStatus::Active,
|
||||
created_at: now - Duration::days(7),
|
||||
updated_at: now - Duration::days(7),
|
||||
voting_starts_at: Some(now - Duration::days(6)),
|
||||
voting_ends_at: Some(now + Duration::days(1)),
|
||||
},
|
||||
Proposal {
|
||||
id: "prop-006".to_string(),
|
||||
creator_id: 5,
|
||||
creator_name: "Omar Makame".to_string(),
|
||||
title: "Zanzibar Renewable Energy Microgrid Network".to_string(),
|
||||
description: "Develop a network of renewable energy microgrids across the Zanzibar Autonomous Zone using tokenized investment and community ownership models. This proposal outlines the technical specifications, governance structure, and token economics for deploying solar and tidal energy systems that will ensure energy independence for the zone.".to_string(),
|
||||
status: ProposalStatus::Active,
|
||||
created_at: now - Duration::days(10),
|
||||
updated_at: now - Duration::days(9),
|
||||
voting_starts_at: Some(now - Duration::days(8)),
|
||||
voting_ends_at: Some(now + Duration::days(6)),
|
||||
},
|
||||
Proposal {
|
||||
id: "prop-007".to_string(),
|
||||
creator_id: 6,
|
||||
creator_name: "Saida Juma".to_string(),
|
||||
title: "ZAZ Educational Technology Initiative".to_string(),
|
||||
description: "Establish a comprehensive educational technology program within the Zanzibar Autonomous Zone to develop local tech talent. This initiative includes coding academies, blockchain development courses, and digital entrepreneurship training, with a focus on preparing Zanzibar's youth for careers in the zone's growing digital economy.".to_string(),
|
||||
status: ProposalStatus::Draft,
|
||||
created_at: now - Duration::days(3),
|
||||
updated_at: now - Duration::days(2),
|
||||
voting_starts_at: None,
|
||||
voting_ends_at: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@ -301,7 +325,7 @@ impl GovernanceController {
|
||||
id: "vote-001".to_string(),
|
||||
proposal_id: proposal_id.to_string(),
|
||||
voter_id: 1,
|
||||
voter_name: "John Doe".to_string(),
|
||||
voter_name: "Robert Callingham".to_string(),
|
||||
vote_type: VoteType::Yes,
|
||||
comment: Some("I strongly support this initiative.".to_string()),
|
||||
created_at: now - Duration::days(2),
|
||||
@ -347,7 +371,7 @@ impl GovernanceController {
|
||||
id: "vote-001".to_string(),
|
||||
proposal_id: "prop-001".to_string(),
|
||||
voter_id: user_id,
|
||||
voter_name: "John Doe".to_string(),
|
||||
voter_name: "Robert Callingham".to_string(),
|
||||
vote_type: VoteType::Yes,
|
||||
comment: Some("I strongly support this initiative.".to_string()),
|
||||
created_at: Utc::now() - Duration::days(2),
|
||||
@ -357,7 +381,7 @@ impl GovernanceController {
|
||||
id: "vote-005".to_string(),
|
||||
proposal_id: "prop-002".to_string(),
|
||||
voter_id: user_id,
|
||||
voter_name: "John Doe".to_string(),
|
||||
voter_name: "Robert Callingham".to_string(),
|
||||
vote_type: VoteType::No,
|
||||
comment: Some("I don't think this is a priority right now.".to_string()),
|
||||
created_at: Utc::now() - Duration::days(10),
|
||||
@ -367,7 +391,7 @@ impl GovernanceController {
|
||||
id: "vote-008".to_string(),
|
||||
proposal_id: "prop-004".to_string(),
|
||||
voter_id: user_id,
|
||||
voter_name: "John Doe".to_string(),
|
||||
voter_name: "Robert Callingham".to_string(),
|
||||
vote_type: VoteType::Yes,
|
||||
comment: None,
|
||||
created_at: Utc::now() - Duration::days(18),
|
||||
@ -377,7 +401,7 @@ impl GovernanceController {
|
||||
id: "vote-010".to_string(),
|
||||
proposal_id: "prop-005".to_string(),
|
||||
voter_id: user_id,
|
||||
voter_name: "John Doe".to_string(),
|
||||
voter_name: "Robert Callingham".to_string(),
|
||||
vote_type: VoteType::Yes,
|
||||
comment: Some("Security is always a top priority.".to_string()),
|
||||
created_at: Utc::now() - Duration::days(5),
|
||||
|
@ -6,5 +6,6 @@ pub mod calendar;
|
||||
pub mod governance;
|
||||
pub mod flow;
|
||||
pub mod contract;
|
||||
pub mod asset;
|
||||
|
||||
// Re-export controllers for easier imports
|
||||
|
282
actix_mvc_app/src/models/asset.rs
Normal file
282
actix_mvc_app/src/models/asset.rs
Normal file
@ -0,0 +1,282 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Asset types representing different categories of digital assets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AssetType {
|
||||
NFT,
|
||||
Token,
|
||||
RealEstate,
|
||||
Commodity,
|
||||
Share,
|
||||
Bond,
|
||||
IntellectualProperty,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl AssetType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
AssetType::NFT => "NFT",
|
||||
AssetType::Token => "Token",
|
||||
AssetType::RealEstate => "Real Estate",
|
||||
AssetType::Commodity => "Commodity",
|
||||
AssetType::Share => "Share",
|
||||
AssetType::Bond => "Bond",
|
||||
AssetType::IntellectualProperty => "Intellectual Property",
|
||||
AssetType::Other => "Other",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status of an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AssetStatus {
|
||||
Active,
|
||||
Locked,
|
||||
ForSale,
|
||||
Transferred,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl AssetStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
AssetStatus::Active => "Active",
|
||||
AssetStatus::Locked => "Locked",
|
||||
AssetStatus::ForSale => "For Sale",
|
||||
AssetStatus::Transferred => "Transferred",
|
||||
AssetStatus::Archived => "Archived",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Blockchain information for an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BlockchainInfo {
|
||||
pub blockchain: String,
|
||||
pub token_id: String,
|
||||
pub contract_address: String,
|
||||
pub owner_address: String,
|
||||
pub transaction_hash: Option<String>,
|
||||
pub block_number: Option<u64>,
|
||||
pub timestamp: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Valuation history point for an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ValuationPoint {
|
||||
pub id: String,
|
||||
pub date: DateTime<Utc>,
|
||||
pub value: f64,
|
||||
pub currency: String,
|
||||
pub source: String,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Transaction history for an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AssetTransaction {
|
||||
pub id: String,
|
||||
pub transaction_type: String,
|
||||
pub date: DateTime<Utc>,
|
||||
pub from_address: Option<String>,
|
||||
pub to_address: Option<String>,
|
||||
pub amount: Option<f64>,
|
||||
pub currency: Option<String>,
|
||||
pub transaction_hash: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Main Asset model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Asset {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub asset_type: AssetType,
|
||||
pub status: AssetStatus,
|
||||
pub owner_id: String,
|
||||
pub owner_name: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub blockchain_info: Option<BlockchainInfo>,
|
||||
pub current_valuation: Option<f64>,
|
||||
pub valuation_currency: Option<String>,
|
||||
pub valuation_date: Option<DateTime<Utc>>,
|
||||
pub valuation_history: Vec<ValuationPoint>,
|
||||
pub transaction_history: Vec<AssetTransaction>,
|
||||
pub metadata: serde_json::Value,
|
||||
pub image_url: Option<String>,
|
||||
pub external_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Creates a new asset
|
||||
pub fn new(
|
||||
name: &str,
|
||||
description: &str,
|
||||
asset_type: AssetType,
|
||||
owner_id: &str,
|
||||
owner_name: &str,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
id: format!("asset-{}", Uuid::new_v4().to_string()[..8].to_string()),
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
asset_type,
|
||||
status: AssetStatus::Active,
|
||||
owner_id: owner_id.to_string(),
|
||||
owner_name: owner_name.to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
blockchain_info: None,
|
||||
current_valuation: None,
|
||||
valuation_currency: None,
|
||||
valuation_date: None,
|
||||
valuation_history: Vec::new(),
|
||||
transaction_history: Vec::new(),
|
||||
metadata: serde_json::json!({}),
|
||||
image_url: None,
|
||||
external_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds blockchain information to the asset
|
||||
pub fn add_blockchain_info(&mut self, blockchain_info: BlockchainInfo) {
|
||||
self.blockchain_info = Some(blockchain_info);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Adds a valuation point to the asset's history
|
||||
pub fn add_valuation(&mut self, value: f64, currency: &str, source: &str, notes: Option<String>) {
|
||||
let valuation = ValuationPoint {
|
||||
id: format!("val-{}", Uuid::new_v4().to_string()[..8].to_string()),
|
||||
date: Utc::now(),
|
||||
value,
|
||||
currency: currency.to_string(),
|
||||
source: source.to_string(),
|
||||
notes,
|
||||
};
|
||||
|
||||
self.current_valuation = Some(value);
|
||||
self.valuation_currency = Some(currency.to_string());
|
||||
self.valuation_date = Some(valuation.date);
|
||||
self.valuation_history.push(valuation);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Adds a transaction to the asset's history
|
||||
pub fn add_transaction(
|
||||
&mut self,
|
||||
transaction_type: &str,
|
||||
from_address: Option<String>,
|
||||
to_address: Option<String>,
|
||||
amount: Option<f64>,
|
||||
currency: Option<String>,
|
||||
transaction_hash: Option<String>,
|
||||
notes: Option<String>,
|
||||
) {
|
||||
let transaction = AssetTransaction {
|
||||
id: format!("tx-{}", Uuid::new_v4().to_string()[..8].to_string()),
|
||||
transaction_type: transaction_type.to_string(),
|
||||
date: Utc::now(),
|
||||
from_address,
|
||||
to_address,
|
||||
amount,
|
||||
currency,
|
||||
transaction_hash,
|
||||
notes,
|
||||
};
|
||||
|
||||
self.transaction_history.push(transaction);
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Updates the status of the asset
|
||||
pub fn update_status(&mut self, status: AssetStatus) {
|
||||
self.status = status;
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
/// Gets the latest valuation point
|
||||
pub fn latest_valuation(&self) -> Option<&ValuationPoint> {
|
||||
self.valuation_history.last()
|
||||
}
|
||||
|
||||
/// Gets the latest transaction
|
||||
pub fn latest_transaction(&self) -> Option<&AssetTransaction> {
|
||||
self.transaction_history.last()
|
||||
}
|
||||
|
||||
/// Gets the valuation history sorted by date
|
||||
pub fn sorted_valuation_history(&self) -> Vec<&ValuationPoint> {
|
||||
let mut history = self.valuation_history.iter().collect::<Vec<_>>();
|
||||
history.sort_by(|a, b| a.date.cmp(&b.date));
|
||||
history
|
||||
}
|
||||
|
||||
/// Gets the transaction history sorted by date
|
||||
pub fn sorted_transaction_history(&self) -> Vec<&AssetTransaction> {
|
||||
let mut history = self.transaction_history.iter().collect::<Vec<_>>();
|
||||
history.sort_by(|a, b| a.date.cmp(&b.date));
|
||||
history
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter for assets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AssetFilter {
|
||||
pub asset_type: Option<AssetType>,
|
||||
pub status: Option<AssetStatus>,
|
||||
pub owner_id: Option<String>,
|
||||
pub min_valuation: Option<f64>,
|
||||
pub max_valuation: Option<f64>,
|
||||
pub valuation_currency: Option<String>,
|
||||
pub search_query: Option<String>,
|
||||
}
|
||||
|
||||
/// Statistics for assets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AssetStatistics {
|
||||
pub total_assets: usize,
|
||||
pub total_value: f64,
|
||||
pub value_by_type: std::collections::HashMap<String, f64>,
|
||||
pub assets_by_type: std::collections::HashMap<String, usize>,
|
||||
pub assets_by_status: std::collections::HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl AssetStatistics {
|
||||
pub fn new(assets: &[Asset]) -> Self {
|
||||
let mut total_value = 0.0;
|
||||
let mut value_by_type = std::collections::HashMap::new();
|
||||
let mut assets_by_type = std::collections::HashMap::new();
|
||||
let mut assets_by_status = std::collections::HashMap::new();
|
||||
|
||||
for asset in assets {
|
||||
if let Some(valuation) = asset.current_valuation {
|
||||
total_value += valuation;
|
||||
|
||||
let asset_type = asset.asset_type.as_str().to_string();
|
||||
*value_by_type.entry(asset_type.clone()).or_insert(0.0) += valuation;
|
||||
*assets_by_type.entry(asset_type).or_insert(0) += 1;
|
||||
} else {
|
||||
let asset_type = asset.asset_type.as_str().to_string();
|
||||
*assets_by_type.entry(asset_type).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let status = asset.status.as_str().to_string();
|
||||
*assets_by_status.entry(status).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
Self {
|
||||
total_assets: assets.len(),
|
||||
total_value,
|
||||
value_by_type,
|
||||
assets_by_type,
|
||||
assets_by_status,
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ pub enum ContractStatus {
|
||||
Draft,
|
||||
PendingSignatures,
|
||||
Signed,
|
||||
Active,
|
||||
Expired,
|
||||
Cancelled
|
||||
}
|
||||
@ -18,6 +19,7 @@ impl ContractStatus {
|
||||
ContractStatus::Draft => "Draft",
|
||||
ContractStatus::PendingSignatures => "Pending Signatures",
|
||||
ContractStatus::Signed => "Signed",
|
||||
ContractStatus::Active => "Active",
|
||||
ContractStatus::Expired => "Expired",
|
||||
ContractStatus::Cancelled => "Cancelled",
|
||||
}
|
||||
@ -31,6 +33,10 @@ pub enum ContractType {
|
||||
Employment,
|
||||
NDA,
|
||||
SLA,
|
||||
Partnership,
|
||||
Distribution,
|
||||
License,
|
||||
Membership,
|
||||
Other
|
||||
}
|
||||
|
||||
@ -41,6 +47,10 @@ impl ContractType {
|
||||
ContractType::Employment => "Employment Contract",
|
||||
ContractType::NDA => "Non-Disclosure Agreement",
|
||||
ContractType::SLA => "Service Level Agreement",
|
||||
ContractType::Partnership => "Partnership Agreement",
|
||||
ContractType::Distribution => "Distribution Agreement",
|
||||
ContractType::License => "License Agreement",
|
||||
ContractType::Membership => "Membership Agreement",
|
||||
ContractType::Other => "Other",
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,12 @@ pub enum FlowType {
|
||||
ServiceActivation,
|
||||
/// Payment processing flow
|
||||
PaymentProcessing,
|
||||
/// Asset tokenization flow
|
||||
AssetTokenization,
|
||||
/// Certification flow
|
||||
Certification,
|
||||
/// License application flow
|
||||
LicenseApplication,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FlowType {
|
||||
@ -46,6 +52,9 @@ impl std::fmt::Display for FlowType {
|
||||
FlowType::UserOnboarding => write!(f, "User Onboarding"),
|
||||
FlowType::ServiceActivation => write!(f, "Service Activation"),
|
||||
FlowType::PaymentProcessing => write!(f, "Payment Processing"),
|
||||
FlowType::AssetTokenization => write!(f, "Asset Tokenization"),
|
||||
FlowType::Certification => write!(f, "Certification"),
|
||||
FlowType::LicenseApplication => write!(f, "License Application"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ pub mod calendar;
|
||||
pub mod governance;
|
||||
pub mod flow;
|
||||
pub mod contract;
|
||||
pub mod asset;
|
||||
|
||||
// Re-export models for easier imports
|
||||
pub use user::User;
|
||||
|
@ -145,8 +145,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_new_user() {
|
||||
let user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
assert_eq!(user.name, "John Doe");
|
||||
let user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
|
||||
assert_eq!(user.name, "Robert Callingham");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
assert!(!user.is_admin());
|
||||
}
|
||||
@ -161,13 +161,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_update_user() {
|
||||
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
user.update(Some("Jane Doe".to_string()), None);
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
let mut user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
|
||||
user.update(Some("Mary Hewell".to_string()), None);
|
||||
assert_eq!(user.name, "Mary Hewell");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
|
||||
user.update(None, Some("jane@example.com".to_string()));
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
assert_eq!(user.name, "Mary Hewell");
|
||||
assert_eq!(user.email, "jane@example.com");
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ 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::middleware::JwtAuth;
|
||||
use crate::SESSION_KEY;
|
||||
|
||||
@ -89,6 +90,21 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.route("/create", web::get().to(ContractController::create_form))
|
||||
.route("/create", web::post().to(ContractController::create))
|
||||
)
|
||||
|
||||
// Asset routes
|
||||
.service(
|
||||
web::scope("/assets")
|
||||
.route("", web::get().to(AssetController::index))
|
||||
.route("/list", web::get().to(AssetController::list))
|
||||
.route("/my", web::get().to(AssetController::my_assets))
|
||||
.route("/create", web::get().to(AssetController::create_form))
|
||||
.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))
|
||||
)
|
||||
);
|
||||
|
||||
// Keep the /protected scope for any future routes that should be under that path
|
||||
|
@ -1,6 +1,7 @@
|
||||
use actix_web::{error, Error, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use tera::{self, Context, Function, Tera, Value};
|
||||
use std::error::Error as StdError;
|
||||
|
||||
// Export modules
|
||||
pub mod redis_service;
|
||||
@ -122,37 +123,76 @@ pub fn render_template(
|
||||
template_name: &str,
|
||||
ctx: &Context,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
println!("DEBUG: Attempting to render template: {}", template_name);
|
||||
|
||||
// Print all context keys for debugging
|
||||
let mut keys = Vec::new();
|
||||
for (key, _) in ctx.clone().into_json().as_object().unwrap().iter() {
|
||||
keys.push(key.clone());
|
||||
}
|
||||
println!("DEBUG: Context keys: {:?}", keys);
|
||||
|
||||
match tmpl.render(template_name, ctx) {
|
||||
Ok(content) => Ok(HttpResponse::Ok().content_type("text/html").body(content)),
|
||||
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: Error details: {:?}", e);
|
||||
|
||||
// Print the error cause chain for better debugging
|
||||
let mut current_error: Option<&dyn StdError> = Some(&e);
|
||||
let mut error_chain = Vec::new();
|
||||
|
||||
while let Some(error) = current_error {
|
||||
error_chain.push(format!("{}", error));
|
||||
current_error = error.source();
|
||||
}
|
||||
|
||||
println!("DEBUG: Error chain: {:?}", error_chain);
|
||||
|
||||
// Log the error
|
||||
log::error!("Template rendering error: {}", e);
|
||||
log::error!("Error details: {:?}", e);
|
||||
log::error!("Context: {:?}", ctx);
|
||||
|
||||
// Create a context for the error template
|
||||
let mut error_ctx = Context::new();
|
||||
error_ctx.insert("error", &format!("Template rendering error: {}", e));
|
||||
error_ctx.insert("error_details", &format!("{:?}", e));
|
||||
error_ctx.insert("error_location", &template_name);
|
||||
// Create a simple error response with more detailed information
|
||||
let error_html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Template Error</title>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }}
|
||||
.error-container {{ border: 1px solid #f5c6cb; background-color: #f8d7da; padding: 20px; border-radius: 5px; }}
|
||||
.error-title {{ color: #721c24; }}
|
||||
.error-details {{ background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-top: 20px; }}
|
||||
pre {{ background-color: #f1f1f1; padding: 10px; overflow: auto; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1 class="error-title">Template Rendering Error</h1>
|
||||
<p>There was an error rendering the template: <strong>{}</strong></p>
|
||||
<div class="error-details">
|
||||
<h3>Error Details:</h3>
|
||||
<pre>{}</pre>
|
||||
<h3>Error Chain:</h3>
|
||||
<pre>{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
template_name,
|
||||
e,
|
||||
error_chain.join("\n")
|
||||
);
|
||||
|
||||
// Try to render the error template
|
||||
match tmpl.render("error.html", &error_ctx) {
|
||||
Ok(error_page) => {
|
||||
// Return the error page with a 500 status code
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.content_type("text/html")
|
||||
.body(error_page))
|
||||
}
|
||||
Err(render_err) => {
|
||||
// If we can't render the error template, log it and return a basic error
|
||||
log::error!("Error rendering error template: {}", render_err);
|
||||
Err(error::ErrorInternalServerError(format!(
|
||||
"Template rendering error: {}. Failed to render error page: {}",
|
||||
e, render_err
|
||||
)))
|
||||
}
|
||||
}
|
||||
println!("DEBUG: Returning simple error page");
|
||||
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.content_type("text/html")
|
||||
.body(error_html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
271
actix_mvc_app/src/views/assets/create.html
Normal file
271
actix_mvc_app/src/views/assets/create.html
Normal file
@ -0,0 +1,271 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create New Digital Asset{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Create New Digital Asset</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">Create New Asset</li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-plus-circle me-1"></i>
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="createAssetForm" method="post" action="/assets/create">
|
||||
<!-- Basic Information -->
|
||||
<div class="mb-4">
|
||||
<h5>Basic Information</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="asset_type" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="asset_type" name="asset_type" required>
|
||||
{% for type_value, type_label in asset_types %}
|
||||
<option value="{{ type_value }}">{{ type_label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="image_url" class="form-label">Image URL (optional)</label>
|
||||
<input type="url" class="form-control" id="image_url" name="image_url">
|
||||
<div class="form-text">URL to an image representing this asset</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="external_url" class="form-label">External URL (optional)</label>
|
||||
<input type="url" class="form-control" id="external_url" name="external_url">
|
||||
<div class="form-text">URL to an external resource for this asset</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blockchain Information -->
|
||||
<div class="mb-4">
|
||||
<h5>Blockchain Information (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_blockchain_info" name="has_blockchain_info">
|
||||
<label class="form-check-label" for="has_blockchain_info">
|
||||
This asset has blockchain information
|
||||
</label>
|
||||
</div>
|
||||
<div id="blockchainInfoSection" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="blockchain" class="form-label">Blockchain</label>
|
||||
<input type="text" class="form-control" id="blockchain" name="blockchain">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="token_id" class="form-label">Token ID</label>
|
||||
<input type="text" class="form-control" id="token_id" name="token_id">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contract_address" class="form-label">Contract Address</label>
|
||||
<input type="text" class="form-control" id="contract_address" name="contract_address">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="owner_address" class="form-label">Owner Address</label>
|
||||
<input type="text" class="form-control" id="owner_address" name="owner_address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="transaction_hash" class="form-label">Transaction Hash (optional)</label>
|
||||
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="block_number" class="form-label">Block Number (optional)</label>
|
||||
<input type="number" class="form-control" id="block_number" name="block_number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Initial Valuation -->
|
||||
<div class="mb-4">
|
||||
<h5>Initial Valuation (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_valuation" name="has_valuation">
|
||||
<label class="form-check-label" for="has_valuation">
|
||||
Add an initial valuation for this asset
|
||||
</label>
|
||||
</div>
|
||||
<div id="valuationSection" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="valuation_notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="valuation_notes" name="valuation_notes" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="mb-4">
|
||||
<h5>Additional Metadata (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_metadata" name="has_metadata">
|
||||
<label class="form-check-label" for="has_metadata">
|
||||
Add additional metadata for this asset
|
||||
</label>
|
||||
</div>
|
||||
<div id="metadataSection" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="metadata" class="form-label">Metadata (JSON format)</label>
|
||||
<textarea class="form-control" id="metadata" name="metadata" rows="5"></textarea>
|
||||
<div class="form-text">Enter additional metadata in JSON format</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="/assets" class="btn btn-secondary me-md-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Create Asset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Toggle blockchain info section
|
||||
const hasBlockchainInfo = document.getElementById('has_blockchain_info');
|
||||
const blockchainInfoSection = document.getElementById('blockchainInfoSection');
|
||||
|
||||
hasBlockchainInfo.addEventListener('change', function() {
|
||||
blockchainInfoSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Toggle valuation section
|
||||
const hasValuation = document.getElementById('has_valuation');
|
||||
const valuationSection = document.getElementById('valuationSection');
|
||||
|
||||
hasValuation.addEventListener('change', function() {
|
||||
valuationSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Toggle metadata section
|
||||
const hasMetadata = document.getElementById('has_metadata');
|
||||
const metadataSection = document.getElementById('metadataSection');
|
||||
|
||||
hasMetadata.addEventListener('change', function() {
|
||||
metadataSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Form validation
|
||||
const form = document.getElementById('createAssetForm');
|
||||
form.addEventListener('submit', function(event) {
|
||||
let isValid = true;
|
||||
|
||||
// Validate required fields
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const description = document.getElementById('description').value.trim();
|
||||
|
||||
if (!name) {
|
||||
isValid = false;
|
||||
document.getElementById('name').classList.add('is-invalid');
|
||||
} else {
|
||||
document.getElementById('name').classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
isValid = false;
|
||||
document.getElementById('description').classList.add('is-invalid');
|
||||
} else {
|
||||
document.getElementById('description').classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate blockchain info if checked
|
||||
if (hasBlockchainInfo.checked) {
|
||||
const blockchain = document.getElementById('blockchain').value.trim();
|
||||
const tokenId = document.getElementById('token_id').value.trim();
|
||||
const contractAddress = document.getElementById('contract_address').value.trim();
|
||||
const ownerAddress = document.getElementById('owner_address').value.trim();
|
||||
|
||||
if (!blockchain || !tokenId || !contractAddress || !ownerAddress) {
|
||||
isValid = false;
|
||||
if (!blockchain) document.getElementById('blockchain').classList.add('is-invalid');
|
||||
if (!tokenId) document.getElementById('token_id').classList.add('is-invalid');
|
||||
if (!contractAddress) document.getElementById('contract_address').classList.add('is-invalid');
|
||||
if (!ownerAddress) document.getElementById('owner_address').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate valuation if checked
|
||||
if (hasValuation.checked) {
|
||||
const value = document.getElementById('value').value.trim();
|
||||
const currency = document.getElementById('currency').value.trim();
|
||||
const source = document.getElementById('source').value.trim();
|
||||
|
||||
if (!value || !currency || !source) {
|
||||
isValid = false;
|
||||
if (!value) document.getElementById('value').classList.add('is-invalid');
|
||||
if (!currency) document.getElementById('currency').classList.add('is-invalid');
|
||||
if (!source) document.getElementById('source').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate metadata if checked
|
||||
if (hasMetadata.checked) {
|
||||
const metadata = document.getElementById('metadata').value.trim();
|
||||
|
||||
if (metadata) {
|
||||
try {
|
||||
JSON.parse(metadata);
|
||||
document.getElementById('metadata').classList.remove('is-invalid');
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
document.getElementById('metadata').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
alert('Please fix the errors in the form before submitting.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
556
actix_mvc_app/src/views/assets/detail.html
Normal file
556
actix_mvc_app/src/views/assets/detail.html
Normal file
@ -0,0 +1,556 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Asset Details - {{ asset.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Asset Details</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">{{ asset.name }}</li>
|
||||
</ol>
|
||||
|
||||
<!-- Asset Overview -->
|
||||
<div class="row">
|
||||
<div class="col-xl-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Asset Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-4">
|
||||
{% if asset.image_url %}
|
||||
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="img-fluid asset-image mb-3">
|
||||
{% else %}
|
||||
<div class="asset-placeholder mb-3">
|
||||
<i class="fas fa-cube fa-5x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{{ asset.name }}</h3>
|
||||
<div>
|
||||
{% if asset.status == "Active" %}
|
||||
<span class="badge bg-success">{{ asset.status }}</span>
|
||||
{% elif asset.status == "For Sale" %}
|
||||
<span class="badge bg-warning">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Locked" %}
|
||||
<span class="badge bg-secondary">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Transferred" %}
|
||||
<span class="badge bg-info">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Archived" %}
|
||||
<span class="badge bg-danger">{{ asset.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ asset.status }}</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-primary">{{ asset.asset_type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Description</h5>
|
||||
<p>{{ asset.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Current Valuation</h5>
|
||||
{% if asset.current_valuation %}
|
||||
<h3 class="text-primary">{{ asset.valuation_currency }}{{ asset.current_valuation }}</h3>
|
||||
<small class="text-muted">Last updated: {{ asset.valuation_date }}</small>
|
||||
{% else %}
|
||||
<p>No valuation available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Owner Information</h5>
|
||||
<p><strong>Owner:</strong> {{ asset.owner_name }}</p>
|
||||
<p><strong>Owner ID:</strong> {{ asset.owner_id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Dates</h5>
|
||||
<p><strong>Created:</strong> {{ asset.created_at }}</p>
|
||||
<p><strong>Last Updated:</strong> {{ asset.updated_at }}</p>
|
||||
</div>
|
||||
|
||||
{% if asset.external_url %}
|
||||
<div class="mb-3">
|
||||
<a href="{{ asset.external_url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt me-1"></i> View External Resource
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#valuationModal">
|
||||
<i class="fas fa-dollar-sign me-1"></i> Add Valuation
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#transactionModal">
|
||||
<i class="fas fa-exchange-alt me-1"></i> Record Transaction
|
||||
</button>
|
||||
<button class="btn btn-warning" type="button" data-bs-toggle="modal" data-bs-target="#statusModal">
|
||||
<i class="fas fa-edit me-1"></i> Change Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-8">
|
||||
<!-- Blockchain Information -->
|
||||
{% if asset.blockchain_info %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-link me-1"></i>
|
||||
Blockchain Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Blockchain:</strong> {{ asset.blockchain_info.blockchain }}</p>
|
||||
<p><strong>Token ID:</strong> {{ asset.blockchain_info.token_id }}</p>
|
||||
<p><strong>Contract Address:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.contract_address }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Owner Address:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.owner_address }}</code>
|
||||
</p>
|
||||
{% if asset.blockchain_info.transaction_hash %}
|
||||
<p><strong>Transaction Hash:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.transaction_hash }}</code>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if asset.blockchain_info.block_number %}
|
||||
<p><strong>Block Number:</strong> {{ asset.blockchain_info.block_number }}</p>
|
||||
{% endif %}
|
||||
{% if asset.blockchain_info.timestamp %}
|
||||
<p><strong>Timestamp:</strong> {{ asset.blockchain_info.timestamp }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Valuation History Chart -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-line me-1"></i>
|
||||
Valuation History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if valuation_history and valuation_history|length > 0 %}
|
||||
<canvas id="valuationChart" width="100%" height="40"></canvas>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No valuation history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation History Table -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-history me-1"></i>
|
||||
Valuation History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if asset.valuation_history and asset.valuation_history|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Value</th>
|
||||
<th>Currency</th>
|
||||
<th>Source</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for valuation in asset.valuation_history %}
|
||||
<tr>
|
||||
<td>{{ valuation.date }}</td>
|
||||
<td>{{ valuation.value }}</td>
|
||||
<td>{{ valuation.currency }}</td>
|
||||
<td>{{ valuation.source }}</td>
|
||||
<td>{% if valuation.notes %}{{ valuation.notes }}{% else %}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No valuation history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction History -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-exchange-alt me-1"></i>
|
||||
Transaction History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if asset.transaction_history and asset.transaction_history|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Hash</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in asset.transaction_history %}
|
||||
<tr>
|
||||
<td>{{ transaction.date }}</td>
|
||||
<td>{{ transaction.transaction_type }}</td>
|
||||
<td>
|
||||
{% if transaction.from_address %}
|
||||
<code class="blockchain-address-small">{{ transaction.from_address }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.to_address %}
|
||||
<code class="blockchain-address-small">{{ transaction.to_address }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.amount %}
|
||||
{{ transaction.currency }}{{ transaction.amount }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.transaction_hash %}
|
||||
<code class="blockchain-address-small">{{ transaction.transaction_hash }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if transaction.notes %}{{ transaction.notes }}{% else %}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No transaction history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation Modal -->
|
||||
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="valuationForm" method="post" action="/assets/{{ asset.id }}/valuation">
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Modal -->
|
||||
<div class="modal fade" id="transactionModal" tabindex="-1" aria-labelledby="transactionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="transactionModalLabel">Record Transaction</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="transactionForm" method="post" action="/assets/{{ asset.id }}/transaction">
|
||||
<div class="mb-3">
|
||||
<label for="transaction_type" class="form-label">Transaction Type</label>
|
||||
<select class="form-select" id="transaction_type" name="transaction_type" required>
|
||||
<option value="Purchase">Purchase</option>
|
||||
<option value="Sale">Sale</option>
|
||||
<option value="Transfer">Transfer</option>
|
||||
<option value="Mint">Mint</option>
|
||||
<option value="Burn">Burn</option>
|
||||
<option value="Licensing">Licensing</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="from_address" class="form-label">From Address</label>
|
||||
<input type="text" class="form-control" id="from_address" name="from_address">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="to_address" class="form-label">To Address</label>
|
||||
<input type="text" class="form-control" id="to_address" name="to_address">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Amount</label>
|
||||
<input type="number" class="form-control" id="amount" name="amount" step="0.01">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="transaction_currency" name="currency" value="USD">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_hash" class="form-label">Transaction Hash</label>
|
||||
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="transaction_notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveTransactionBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm" method="post" action="/assets/{{ asset.id }}/status/">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Status</label>
|
||||
<select class="form-select" id="newStatus" name="status">
|
||||
<option value="Active" {% if asset.status == "Active" %}selected{% endif %}>Active</option>
|
||||
<option value="Locked" {% if asset.status == "Locked" %}selected{% endif %}>Locked</option>
|
||||
<option value="ForSale" {% if asset.status == "For Sale" %}selected{% endif %}>For Sale</option>
|
||||
<option value="Transferred" {% if asset.status == "Transferred" %}selected{% endif %}>Transferred</option>
|
||||
<option value="Archived" {% if asset.status == "Archived" %}selected{% endif %}>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Valuation History Chart
|
||||
{% if valuation_history and valuation_history|length > 0 %}
|
||||
const ctx = document.getElementById('valuationChart');
|
||||
|
||||
const dates = [
|
||||
{% for point in valuation_history %}
|
||||
"{{ point.date }}"{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const values = [
|
||||
{% for point in valuation_history %}
|
||||
{{ point.value }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: [{
|
||||
label: 'Valuation ({{ valuation_history[0].currency }})',
|
||||
data: values,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: "rgba(78, 115, 223, 0.05)",
|
||||
borderColor: "rgba(78, 115, 223, 1)",
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: "rgba(78, 115, 223, 1)",
|
||||
pointBorderColor: "rgba(78, 115, 223, 1)",
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
|
||||
pointHoverBorderColor: "rgba(78, 115, 223, 1)",
|
||||
pointHitRadius: 10,
|
||||
pointBorderWidth: 2,
|
||||
fill: true
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 7
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
maxTicksLimit: 5,
|
||||
padding: 10,
|
||||
callback: function(value, index, values) {
|
||||
return '{{ valuation_history[0].currency }}' + value;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: "rgb(234, 236, 244)",
|
||||
zeroLineColor: "rgb(234, 236, 244)",
|
||||
drawBorder: false,
|
||||
borderDash: [2],
|
||||
zeroLineBorderDash: [2]
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "rgb(255,255,255)",
|
||||
bodyFontColor: "#858796",
|
||||
titleMarginBottom: 10,
|
||||
titleFontColor: '#6e707e',
|
||||
titleFontSize: 14,
|
||||
borderColor: '#dddfeb',
|
||||
borderWidth: 1,
|
||||
xPadding: 15,
|
||||
yPadding: 15,
|
||||
displayColors: false,
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
caretPadding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
var label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += '{{ valuation_history[0].currency }}' + context.parsed.y;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
// Form submission handlers
|
||||
const saveValuationBtn = document.getElementById('saveValuationBtn');
|
||||
if (saveValuationBtn) {
|
||||
saveValuationBtn.addEventListener('click', function() {
|
||||
document.getElementById('valuationForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
const saveTransactionBtn = document.getElementById('saveTransactionBtn');
|
||||
if (saveTransactionBtn) {
|
||||
saveTransactionBtn.addEventListener('click', function() {
|
||||
document.getElementById('transactionForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
const saveStatusBtn = document.getElementById('saveStatusBtn');
|
||||
if (saveStatusBtn) {
|
||||
saveStatusBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('statusForm');
|
||||
const newStatus = document.getElementById('newStatus').value;
|
||||
form.action = form.action + newStatus;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.asset-image {
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.asset-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.blockchain-address {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.blockchain-address-small {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
140
actix_mvc_app/src/views/assets/index.html
Normal file
140
actix_mvc_app/src/views/assets/index.html
Normal file
@ -0,0 +1,140 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Digital Assets Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Digital Assets Dashboard</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item active">Digital Assets</li>
|
||||
</ol>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-primary text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">{{ stats.total_assets }}</h2>
|
||||
<p class="mb-0">Total Assets</p>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center justify-content-between">
|
||||
<a class="small text-white stretched-link" href="/assets/list">View All Assets</a>
|
||||
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-success text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">${{ stats.total_value }}</h2>
|
||||
<p class="mb-0">Total Valuation</p>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center justify-content-between">
|
||||
<a class="small text-white stretched-link" href="/assets/list">View Details</a>
|
||||
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-warning text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">{{ stats.assets_by_status.Active }}</h2>
|
||||
<p class="mb-0">Active Assets</p>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center justify-content-between">
|
||||
<a class="small text-white stretched-link" href="/assets/list">View Active Assets</a>
|
||||
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-danger text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">0</h2>
|
||||
<p class="mb-0">Pending Transactions</p>
|
||||
</div>
|
||||
<div class="card-footer d-flex align-items-center justify-content-between">
|
||||
<a class="small text-white stretched-link" href="/assets/list">View Transactions</a>
|
||||
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Types Distribution -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-pie-chart me-1"></i>
|
||||
Asset Types Distribution
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset Type</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset_type in assets_by_type %}
|
||||
<tr>
|
||||
<td>{{ asset_type.type }}</td>
|
||||
<td>{{ asset_type.count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Assets Table -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-table me-1"></i>
|
||||
Recent Assets
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset in recent_assets %}
|
||||
<tr>
|
||||
<td>{{ asset.name }}</td>
|
||||
<td>{{ asset.asset_type }}</td>
|
||||
<td>{{ asset.status }}</td>
|
||||
<td>
|
||||
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="/assets/list" class="btn btn-primary">View All Assets</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
286
actix_mvc_app/src/views/assets/list.html
Normal file
286
actix_mvc_app/src/views/assets/list.html
Normal file
@ -0,0 +1,286 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Digital Assets List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Digital Assets</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">All Assets</li>
|
||||
</ol>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-filter me-1"></i>
|
||||
Filter Assets
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="assetTypeFilter" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="assetTypeFilter">
|
||||
<option value="all">All Types</option>
|
||||
<option value="NFT">NFT</option>
|
||||
<option value="Token">Token</option>
|
||||
<option value="RealEstate">Real Estate</option>
|
||||
<option value="Commodity">Commodity</option>
|
||||
<option value="Share">Share</option>
|
||||
<option value="Bond">Bond</option>
|
||||
<option value="IntellectualProperty">Intellectual Property</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="statusFilter" class="form-label">Status</label>
|
||||
<select class="form-select" id="statusFilter">
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Locked">Locked</option>
|
||||
<option value="ForSale">For Sale</option>
|
||||
<option value="Transferred">Transferred</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="valuationFilter" class="form-label">Valuation</label>
|
||||
<select class="form-select" id="valuationFilter">
|
||||
<option value="all">All Valuations</option>
|
||||
<option value="under1000">Under $1,000</option>
|
||||
<option value="1000to10000">$1,000 - $10,000</option>
|
||||
<option value="10000to100000">$10,000 - $100,000</option>
|
||||
<option value="over100000">Over $100,000</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="searchInput" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="Search by name or description">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button id="applyFilters" class="btn btn-primary">Apply Filters</button>
|
||||
<button id="resetFilters" class="btn btn-secondary">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Table -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-table me-1"></i>
|
||||
All Digital Assets
|
||||
</div>
|
||||
<div>
|
||||
<a href="/assets/create" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus"></i> Create New Asset
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover" id="assetsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Owner</th>
|
||||
<th>Valuation</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset in assets %}
|
||||
<tr class="asset-row"
|
||||
data-type="{{ asset.asset_type }}"
|
||||
data-status="{{ asset.status }}"
|
||||
data-valuation="{% if asset.current_valuation %}{{ asset.current_valuation }}{% else %}0{% endif %}">
|
||||
<td>
|
||||
{% if asset.image_url %}
|
||||
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="asset-thumbnail me-2">
|
||||
{% endif %}
|
||||
{{ asset.name }}
|
||||
</td>
|
||||
<td>{{ asset.asset_type }}</td>
|
||||
<td>
|
||||
{% if asset.status == "Active" %}
|
||||
<span class="badge bg-success">{{ asset.status }}</span>
|
||||
{% elif asset.status == "For Sale" %}
|
||||
<span class="badge bg-warning">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Locked" %}
|
||||
<span class="badge bg-secondary">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Transferred" %}
|
||||
<span class="badge bg-info">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Archived" %}
|
||||
<span class="badge bg-danger">{{ asset.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ asset.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ asset.owner_name }}</td>
|
||||
<td>
|
||||
{% if asset.current_valuation %}
|
||||
{{ asset.valuation_currency }}{{ asset.current_valuation }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ asset.created_at }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if asset.status == "Active" %}
|
||||
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#statusModal" data-asset-id="{{ asset.id }}">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Change Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm" method="post" action="">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Status</label>
|
||||
<select class="form-select" id="newStatus" name="status">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Locked">Locked</option>
|
||||
<option value="ForSale">For Sale</option>
|
||||
<option value="Transferred">Transferred</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Status modal functionality
|
||||
const statusModal = document.getElementById('statusModal');
|
||||
if (statusModal) {
|
||||
statusModal.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const assetId = button.getAttribute('data-asset-id');
|
||||
const form = document.getElementById('statusForm');
|
||||
form.action = `/assets/${assetId}/status/`;
|
||||
});
|
||||
|
||||
const saveStatusBtn = document.getElementById('saveStatusBtn');
|
||||
saveStatusBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('statusForm');
|
||||
const newStatus = document.getElementById('newStatus').value;
|
||||
form.action = form.action + newStatus;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Filtering functionality
|
||||
const applyFilters = document.getElementById('applyFilters');
|
||||
if (applyFilters) {
|
||||
applyFilters.addEventListener('click', function() {
|
||||
filterAssets();
|
||||
});
|
||||
}
|
||||
|
||||
const resetFilters = document.getElementById('resetFilters');
|
||||
if (resetFilters) {
|
||||
resetFilters.addEventListener('click', function() {
|
||||
document.getElementById('assetTypeFilter').value = 'all';
|
||||
document.getElementById('statusFilter').value = 'all';
|
||||
document.getElementById('valuationFilter').value = 'all';
|
||||
document.getElementById('searchInput').value = '';
|
||||
filterAssets();
|
||||
});
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keyup', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
filterAssets();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterAssets() {
|
||||
const typeFilter = document.getElementById('assetTypeFilter').value;
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const valuationFilter = document.getElementById('valuationFilter').value;
|
||||
const searchText = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
const rows = document.querySelectorAll('#assetsTable tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const type = row.getAttribute('data-type');
|
||||
const status = row.getAttribute('data-status');
|
||||
const valuation = parseFloat(row.getAttribute('data-valuation'));
|
||||
const name = row.querySelector('td:first-child').textContent.toLowerCase();
|
||||
|
||||
let typeMatch = typeFilter === 'all' || type === typeFilter;
|
||||
let statusMatch = statusFilter === 'all' || status === statusFilter;
|
||||
let searchMatch = searchText === '' || name.includes(searchText);
|
||||
|
||||
let valuationMatch = true;
|
||||
if (valuationFilter === 'under1000') {
|
||||
valuationMatch = valuation < 1000;
|
||||
} else if (valuationFilter === '1000to10000') {
|
||||
valuationMatch = valuation >= 1000 && valuation < 10000;
|
||||
} else if (valuationFilter === '10000to100000') {
|
||||
valuationMatch = valuation >= 10000 && valuation < 100000;
|
||||
} else if (valuationFilter === 'over100000') {
|
||||
valuationMatch = valuation >= 100000;
|
||||
}
|
||||
|
||||
if (typeMatch && statusMatch && valuationMatch && searchMatch) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.asset-thumbnail {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
373
actix_mvc_app/src/views/assets/my_assets.html
Normal file
373
actix_mvc_app/src/views/assets/my_assets.html
Normal file
@ -0,0 +1,373 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}My Digital Assets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">My Digital Assets</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">My Assets</li>
|
||||
</ol>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-primary text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">{{ assets | length }}</h2>
|
||||
<p class="mb-0">Total Assets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-success text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set active_count = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.status == "Active" %}
|
||||
{% set active_count = active_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">{{ active_count }}</h2>
|
||||
<p class="mb-0">Active Assets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-warning text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set for_sale_count = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.status == "For Sale" %}
|
||||
{% set for_sale_count = for_sale_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">{{ for_sale_count }}</h2>
|
||||
<p class="mb-0">For Sale</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-info text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set total_value = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.current_valuation %}
|
||||
{% set total_value = total_value + asset.current_valuation %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">${% if total_value %}{{ total_value }}{% else %}0.00{% endif %}</h2>
|
||||
<p class="mb-0">Total Value</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Table -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-table me-1"></i>
|
||||
My Digital Assets
|
||||
</div>
|
||||
<div>
|
||||
<a href="/assets/create" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus"></i> Create New Asset
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if assets and assets|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover" id="myAssetsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Valuation</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset in assets %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if asset.image_url %}
|
||||
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="asset-thumbnail me-2">
|
||||
{% endif %}
|
||||
{{ asset.name }}
|
||||
</td>
|
||||
<td>{{ asset.asset_type }}</td>
|
||||
<td>
|
||||
{% if asset.status == "Active" %}
|
||||
<span class="badge bg-success">{{ asset.status }}</span>
|
||||
{% elif asset.status == "For Sale" %}
|
||||
<span class="badge bg-warning">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Locked" %}
|
||||
<span class="badge bg-secondary">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Transferred" %}
|
||||
<span class="badge bg-info">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Archived" %}
|
||||
<span class="badge bg-danger">{{ asset.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ asset.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if asset.current_valuation %}
|
||||
{{ asset.valuation_currency }}{{ asset.current_valuation }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ asset.created_at }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#statusModal" data-asset-id="{{ asset.id }}">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#valuationModal" data-asset-id="{{ asset.id }}">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<p>You don't have any digital assets yet.</p>
|
||||
<a href="/assets/create" class="btn btn-primary">Create Your First Asset</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Types Distribution -->
|
||||
<div class="row">
|
||||
<div class="col-xl-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-pie me-1"></i>
|
||||
Asset Types Distribution
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="assetTypesChart" width="100%" height="40"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-bar me-1"></i>
|
||||
Asset Value Distribution
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="assetValueChart" width="100%" height="40"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Change Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm" method="post" action="">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Status</label>
|
||||
<select class="form-select" id="newStatus" name="status">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Locked">Locked</option>
|
||||
<option value="ForSale">For Sale</option>
|
||||
<option value="Transferred">Transferred</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation Modal -->
|
||||
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="valuationForm" method="post" action="">
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Status modal functionality
|
||||
const statusModal = document.getElementById('statusModal');
|
||||
if (statusModal) {
|
||||
statusModal.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const assetId = button.getAttribute('data-asset-id');
|
||||
const form = document.getElementById('statusForm');
|
||||
form.action = `/assets/${assetId}/status/`;
|
||||
});
|
||||
|
||||
const saveStatusBtn = document.getElementById('saveStatusBtn');
|
||||
saveStatusBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('statusForm');
|
||||
const newStatus = document.getElementById('newStatus').value;
|
||||
form.action = form.action + newStatus;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Valuation modal functionality
|
||||
const valuationModal = document.getElementById('valuationModal');
|
||||
if (valuationModal) {
|
||||
valuationModal.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const assetId = button.getAttribute('data-asset-id');
|
||||
const form = document.getElementById('valuationForm');
|
||||
form.action = `/assets/${assetId}/valuation`;
|
||||
});
|
||||
|
||||
const saveValuationBtn = document.getElementById('saveValuationBtn');
|
||||
saveValuationBtn.addEventListener('click', function() {
|
||||
document.getElementById('valuationForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Asset Types Chart
|
||||
const assetTypesCtx = document.getElementById('assetTypesChart');
|
||||
if (assetTypesCtx) {
|
||||
// Count assets by type
|
||||
const assetTypes = {};
|
||||
{% for asset in assets %}
|
||||
if (!assetTypes['{{ asset.asset_type }}']) {
|
||||
assetTypes['{{ asset.asset_type }}'] = 0;
|
||||
}
|
||||
assetTypes['{{ asset.asset_type }}']++;
|
||||
{% endfor %}
|
||||
|
||||
const typeLabels = Object.keys(assetTypes);
|
||||
const typeCounts = Object.values(assetTypes);
|
||||
|
||||
new Chart(assetTypesCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: typeLabels,
|
||||
datasets: [{
|
||||
data: typeCounts,
|
||||
backgroundColor: [
|
||||
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e',
|
||||
'#e74a3b', '#858796', '#5a5c69', '#2c9faf'
|
||||
],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Asset Value Chart
|
||||
const assetValueCtx = document.getElementById('assetValueChart');
|
||||
if (assetValueCtx) {
|
||||
// Prepare data for assets with valuation
|
||||
const assetNames = [];
|
||||
const assetValues = [];
|
||||
|
||||
{% for asset in assets %}
|
||||
{% if asset.current_valuation %}
|
||||
assetNames.push('{{ asset.name }}');
|
||||
assetValues.push({{ asset.current_valuation }});
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
new Chart(assetValueCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: assetNames,
|
||||
datasets: [{
|
||||
label: 'Asset Value ($)',
|
||||
data: assetValues,
|
||||
backgroundColor: '#4e73df',
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.asset-thumbnail {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@ -76,6 +76,7 @@
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="/tickets/new">New Ticket</a></li>
|
||||
<li><a class="dropdown-item" href="/my-tickets">My Tickets</a></li>
|
||||
<li><a class="dropdown-item" href="/assets/my">My Assets</a></li>
|
||||
<li><a class="dropdown-item" href="/governance/my-votes">My Votes</a></li>
|
||||
{% if user.role == "Admin" %}
|
||||
<li><a class="dropdown-item" href="/admin">Admin Panel</a></li>
|
||||
@ -127,6 +128,11 @@
|
||||
<i class="bi bi-file-earmark-text me-2"></i> Contracts
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'assets' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/assets">
|
||||
<i class="bi bi-coin me-2"></i> Digital Assets
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'editor' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/editor">
|
||||
<i class="bi bi-markdown me-2"></i> Markdown Editor
|
||||
@ -169,7 +175,7 @@
|
||||
<span>Convenience, Safety and Privacy</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>© {{ now(year=true) }} Zanzibar Autonomous Zone. All rights reserved.</span>
|
||||
<span>© 2024 Zanzibar Autonomous Zone. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -24,302 +24,309 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Overview -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Overview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Status:</div>
|
||||
<div class="col-md-9">
|
||||
<span class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Type:</div>
|
||||
<div class="col-md-9">{{ contract.contract_type }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Created By:</div>
|
||||
<div class="col-md-9">{{ contract.created_by }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Created:</div>
|
||||
<div class="col-md-9">{{ contract.created_at }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Last Updated:</div>
|
||||
<div class="col-md-9">{{ contract.updated_at }}</div>
|
||||
</div>
|
||||
{% if contract.effective_date %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Effective Date:</div>
|
||||
<div class="col-md-9">{{ contract.effective_date }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if contract.expiration_date %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Expiration Date:</div>
|
||||
<div class="col-md-9">{{ contract.expiration_date }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Version:</div>
|
||||
<div class="col-md-9">{{ contract.current_version }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 fw-bold">Description:</div>
|
||||
<div class="col-md-9">{{ contract.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if contract.status == 'Draft' %}
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-primary">
|
||||
<i class="bi bi-pencil me-1"></i> Edit Contract
|
||||
</a>
|
||||
<a href="/contracts/{{ contract.id }}/send" class="btn btn-success">
|
||||
<i class="bi bi-send me-1"></i> Send for Signatures
|
||||
</a>
|
||||
<button class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
{% elif contract.status == 'PendingSignatures' %}
|
||||
<button class="btn btn-success">
|
||||
<i class="bi bi-pen me-1"></i> Sign Contract
|
||||
</button>
|
||||
<button class="btn btn-warning">
|
||||
<i class="bi bi-x-circle me-1"></i> Reject Contract
|
||||
</button>
|
||||
<button class="btn btn-secondary">
|
||||
<i class="bi bi-send me-1"></i> Resend Invitations
|
||||
</button>
|
||||
{% elif contract.status == 'Signed' %}
|
||||
<button class="btn btn-primary">
|
||||
<i class="bi bi-download me-1"></i> Download Contract
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary">
|
||||
<i class="bi bi-files me-1"></i> Clone Contract
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contract Tabs -->
|
||||
<ul class="nav nav-tabs mb-4" id="contractTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="document-tab" data-bs-toggle="tab" data-bs-target="#document" type="button" role="tab" aria-controls="document" aria-selected="true">
|
||||
<i class="bi bi-file-text me-1"></i> Document
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="false">
|
||||
<i class="bi bi-info-circle me-1"></i> Details
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="activity-tab" data-bs-toggle="tab" data-bs-target="#activity" type="button" role="tab" aria-controls="activity" aria-selected="false">
|
||||
<i class="bi bi-clock-history me-1"></i> Activity
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Contract Content -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Contract Content</h5>
|
||||
{% if contract.revisions|length > 1 %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="versionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Version {{ contract.current_version }}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="versionDropdown">
|
||||
{% for revision in contract.revisions %}
|
||||
<li><a class="dropdown-item" href="/contracts/{{ contract.id }}?version={{ revision.version }}">Version {{ revision.version }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract.revisions|length > 0 %}
|
||||
{% set latest_revision = contract.latest_revision %}
|
||||
<div class="contract-content border p-3 bg-light">
|
||||
{{ latest_revision.content|safe }}
|
||||
</div>
|
||||
<div class="mt-3 text-muted small">
|
||||
<p>Last updated by {{ latest_revision.created_by }} on {{ latest_revision.created_at }}</p>
|
||||
{% if latest_revision.comments %}
|
||||
<p><strong>Comments:</strong> {{ latest_revision.comments }}</p>
|
||||
<div class="tab-content" id="contractTabsContent">
|
||||
<!-- Document Tab -->
|
||||
<div class="tab-pane fade show active" id="document" role="tabpanel" aria-labelledby="document-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<!-- Document View -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Contract Document</h5>
|
||||
{% if contract.status == 'Signed' %}
|
||||
<span class="badge bg-success">SIGNED</span>
|
||||
{% elif contract.status == 'Active' %}
|
||||
<span class="badge bg-success">ACTIVE</span>
|
||||
{% elif contract.status == 'PendingSignatures' %}
|
||||
<span class="badge bg-warning text-dark">PENDING</span>
|
||||
{% elif contract.status == 'Draft' %}
|
||||
<span class="badge bg-secondary">DRAFT</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3 text-muted">
|
||||
<p>No content has been added to this contract yet.</p>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-pencil me-1"></i> Add Content
|
||||
</a>
|
||||
<div class="card-body bg-light">
|
||||
{% if contract.revisions|length > 0 %}
|
||||
{% set latest_revision = contract.latest_revision %}
|
||||
<div class="bg-white p-4 border rounded">
|
||||
{{ latest_revision.content|safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5 text-muted">
|
||||
<p>No content has been added to this contract yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signers -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Signers</h5>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<button class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus me-1"></i> Add Signer
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract.signers|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Signed Date</th>
|
||||
<th>Comments</th>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<th>Actions</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</div>
|
||||
|
||||
<!-- Signature Areas -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Signatures</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{% for signer in contract.signers %}
|
||||
<tr>
|
||||
<td>{{ signer.name }}</td>
|
||||
<td>{{ signer.email }}</td>
|
||||
<td>
|
||||
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
|
||||
{{ signer.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if signer.signed_at %}
|
||||
{{ signer.signed_at }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if signer.comments %}
|
||||
{{ signer.comments }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100 {% if signer.status == 'Signed' %}border-success{% elif signer.status == 'Rejected' %}border-danger{% else %}border-warning{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">{{ signer.name }}</h6>
|
||||
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
|
||||
{{ signer.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-2">{{ signer.email }}</p>
|
||||
|
||||
{% if signer.status == 'Signed' %}
|
||||
<div class="text-center border-top pt-3">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Signature_of_John_Hancock.svg/1280px-Signature_of_John_Hancock.svg.png" alt="Signature" class="img-fluid" style="max-height: 60px;">
|
||||
<div class="small text-muted mt-2">Signed on {{ signer.signed_at }}</div>
|
||||
</div>
|
||||
{% elif signer.status == 'Rejected' %}
|
||||
<div class="alert alert-danger mt-3">
|
||||
<i class="bi bi-x-circle me-2"></i> Rejected on {{ signer.signed_at }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center mt-3">
|
||||
<p class="text-muted mb-2">Waiting for signature...</p>
|
||||
{% if not user_has_signed %}
|
||||
<button class="btn btn-primary btn-sm btn-sign" data-signer-id="{{ signer.id }}">
|
||||
<i class="bi bi-pen me-1"></i> Sign Here
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if signer.comments %}
|
||||
<div class="mt-3">
|
||||
<p class="small text-muted mb-1">Comments:</p>
|
||||
<p class="small">{{ signer.comments }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3 text-muted">
|
||||
<p>No signers have been added to this contract yet.</p>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<button class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus me-1"></i> Add Signer
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if contract.status == 'Draft' %}
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-primary">
|
||||
<i class="bi bi-pencil me-1"></i> Edit Contract
|
||||
</a>
|
||||
<a href="/contracts/{{ contract.id }}/send" class="btn btn-success">
|
||||
<i class="bi bi-send me-1"></i> Send for Signatures
|
||||
</a>
|
||||
<button class="btn btn-danger">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
{% elif contract.status == 'PendingSignatures' %}
|
||||
<button class="btn btn-success">
|
||||
<i class="bi bi-pen me-1"></i> Sign Contract
|
||||
</button>
|
||||
<button class="btn btn-warning">
|
||||
<i class="bi bi-x-circle me-1"></i> Reject Contract
|
||||
</button>
|
||||
<button class="btn btn-secondary">
|
||||
<i class="bi bi-send me-1"></i> Resend Invitations
|
||||
</button>
|
||||
{% elif contract.status == 'Signed' or contract.status == 'Active' %}
|
||||
<button class="btn btn-primary">
|
||||
<i class="bi bi-download me-1"></i> Download Contract
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary">
|
||||
<i class="bi bi-files me-1"></i> Clone Contract
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Signers Status</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for signer in contract.signers %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div>{{ signer.name }}</div>
|
||||
<small class="text-muted">{{ signer.email }}</small>
|
||||
</div>
|
||||
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
|
||||
{{ signer.status }}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>Total: {{ contract.signers|length }}</span>
|
||||
<span>Signed: {{ signed_signers }} / Pending: {{ pending_signers }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Status:</strong>
|
||||
<span class="badge {% if contract.status == 'Signed' or contract.status == 'Active' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</p>
|
||||
<p><strong>Type:</strong> {{ contract.contract_type }}</p>
|
||||
<p><strong>Created:</strong> {{ contract.created_at }}</p>
|
||||
<p><strong>Version:</strong> {{ contract.current_version }}</p>
|
||||
{% if contract.effective_date %}
|
||||
<p><strong>Effective:</strong> {{ contract.effective_date }}</p>
|
||||
{% endif %}
|
||||
{% if contract.expiration_date %}
|
||||
<p><strong>Expires:</strong> {{ contract.expiration_date }}</p>
|
||||
{% endif %}
|
||||
{% if contract.organization %}
|
||||
<p><strong>Organization:</strong> {{ contract.organization }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Revisions -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Revisions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract.revisions|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Date</th>
|
||||
<th>Notes</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for revision in contract.revisions %}
|
||||
<tr>
|
||||
<td>{{ revision.version }}</td>
|
||||
<td>{{ revision.created_at }}</td>
|
||||
<td>{{ revision.notes }}</td>
|
||||
<td>
|
||||
<a href="/contracts/{{ contract.id }}/revisions/{{ revision.version }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Details Tab -->
|
||||
<div class="tab-pane fade" id="details" role="tabpanel" aria-labelledby="details-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Overview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Status:</div>
|
||||
<div class="col-md-9">
|
||||
<span class="badge {% if contract.status == 'Signed' or contract.status == 'Active' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Type:</div>
|
||||
<div class="col-md-9">{{ contract.contract_type }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Created By:</div>
|
||||
<div class="col-md-9">{{ contract.created_by }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Created:</div>
|
||||
<div class="col-md-9">{{ contract.created_at }}</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Last Updated:</div>
|
||||
<div class="col-md-9">{{ contract.updated_at }}</div>
|
||||
</div>
|
||||
{% if contract.effective_date %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Effective Date:</div>
|
||||
<div class="col-md-9">{{ contract.effective_date }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if contract.expiration_date %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Expiration Date:</div>
|
||||
<div class="col-md-9">{{ contract.expiration_date }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3 fw-bold">Version:</div>
|
||||
<div class="col-md-9">{{ contract.current_version }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 fw-bold">Description:</div>
|
||||
<div class="col-md-9">{{ contract.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Organization</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract.organization %}
|
||||
<p><strong>{{ contract.organization }}</strong></p>
|
||||
<p class="text-muted">
|
||||
<i class="bi bi-building me-1"></i> Registered in Zanzibar Autonomous Zone
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-muted">No organization specified</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No revisions available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revision History -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
|
||||
<!-- Contract Revisions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Revision History</h5>
|
||||
<h5 class="mb-0">Contract Revisions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract.revisions|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Date</th>
|
||||
<th>Created By</th>
|
||||
<th>Created Date</th>
|
||||
<th>Comments</th>
|
||||
<th>Notes</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for revision in contract.revisions|reverse %}
|
||||
{% for revision in contract.revisions %}
|
||||
<tr>
|
||||
<td>{{ revision.version }}</td>
|
||||
<td>{{ revision.created_by }}</td>
|
||||
<td>{{ revision.created_at }}</td>
|
||||
<td>{{ revision.created_by }}</td>
|
||||
<td>{{ revision.notes }}</td>
|
||||
<td>
|
||||
{% if revision.comments %}
|
||||
{{ revision.comments }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/contracts/{{ contract.id }}?version={{ revision.version }}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-eye"></i>
|
||||
<a href="/contracts/{{ contract.id }}?version={{ revision.version }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-eye"></i> View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@ -328,13 +335,213 @@
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3 text-muted">
|
||||
<p>No revisions have been made to this contract yet.</p>
|
||||
</div>
|
||||
<p class="text-muted">No revisions available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Tab -->
|
||||
<div class="tab-pane fade" id="activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Activity Timeline</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item border-start border-4 border-primary">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">Contract Created</h6>
|
||||
<small>{{ contract.created_at }}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ contract.created_by }} created this contract</p>
|
||||
</li>
|
||||
|
||||
{% for revision in contract.revisions %}
|
||||
{% if revision.version > 1 %}
|
||||
<li class="list-group-item border-start border-4 border-info">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">Contract Updated to Version {{ revision.version }}</h6>
|
||||
<small>{{ revision.created_at }}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ revision.created_by }} updated the contract</p>
|
||||
{% if revision.notes %}
|
||||
<p class="mb-0 text-muted fst-italic">{{ revision.notes }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for signer in contract.signers %}
|
||||
{% if signer.status == 'Signed' %}
|
||||
<li class="list-group-item border-start border-4 border-success">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">Contract Signed</h6>
|
||||
<small>{{ signer.signed_at }}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ signer.name }} signed the contract</p>
|
||||
{% if signer.comments %}
|
||||
<p class="mb-0 text-muted fst-italic">{{ signer.comments }}</p>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if contract.status == 'Active' %}
|
||||
<li class="list-group-item border-start border-4 border-success">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h6 class="mb-1">Contract Activated</h6>
|
||||
<small>{{ contract.updated_at }}</small>
|
||||
</div>
|
||||
<p class="mb-1">Contract became active after all parties signed</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Modal -->
|
||||
<div class="modal fade" id="signatureModal" tabindex="-1" aria-labelledby="signatureModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="signatureModalLabel">Sign Contract</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="signatureCanvas" class="form-label">Draw your signature below:</label>
|
||||
<div class="border p-2">
|
||||
<canvas id="signatureCanvas" width="450" height="150" style="border: 1px solid #ddd; width: 100%;"></canvas>
|
||||
</div>
|
||||
<div class="mt-2 text-end">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="clearSignature">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="signatureComments" class="form-label">Comments (optional):</label>
|
||||
<textarea class="form-control" id="signatureComments" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="submitSignature">Sign Contract</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Signature canvas functionality
|
||||
const canvas = document.getElementById('signatureCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let isDrawing = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
// Set up canvas
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.strokeStyle = '#000';
|
||||
|
||||
// Drawing functions
|
||||
function startDrawing(e) {
|
||||
isDrawing = true;
|
||||
[lastX, lastY] = [e.offsetX, e.offsetY];
|
||||
}
|
||||
|
||||
function draw(e) {
|
||||
if (!isDrawing) return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastX, lastY);
|
||||
ctx.lineTo(e.offsetX, e.offsetY);
|
||||
ctx.stroke();
|
||||
[lastX, lastY] = [e.offsetX, e.offsetY];
|
||||
}
|
||||
|
||||
function stopDrawing() {
|
||||
isDrawing = false;
|
||||
}
|
||||
|
||||
// Event listeners for canvas
|
||||
canvas.addEventListener('mousedown', startDrawing);
|
||||
canvas.addEventListener('mousemove', draw);
|
||||
canvas.addEventListener('mouseup', stopDrawing);
|
||||
canvas.addEventListener('mouseout', stopDrawing);
|
||||
|
||||
// Touch support
|
||||
canvas.addEventListener('touchstart', function(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const mouseEvent = new MouseEvent('mousedown', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchmove', function(e) {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
const mouseEvent = new MouseEvent('mousemove', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
});
|
||||
|
||||
canvas.addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
const mouseEvent = new MouseEvent('mouseup', {});
|
||||
canvas.dispatchEvent(mouseEvent);
|
||||
});
|
||||
|
||||
// Clear signature
|
||||
document.getElementById('clearSignature').addEventListener('click', function() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
|
||||
// Sign buttons
|
||||
const signButtons = document.querySelectorAll('.btn-sign');
|
||||
let currentSignerId = null;
|
||||
|
||||
signButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
currentSignerId = this.dataset.signerId;
|
||||
const signatureModal = new bootstrap.Modal(document.getElementById('signatureModal'));
|
||||
signatureModal.show();
|
||||
});
|
||||
});
|
||||
|
||||
// Submit signature
|
||||
document.getElementById('submitSignature').addEventListener('click', function() {
|
||||
// In a real app, we would send the signature to the server
|
||||
// For demo, we'll just simulate a successful signature
|
||||
|
||||
// Get the signature image
|
||||
const signatureImage = canvas.toDataURL();
|
||||
const comments = document.getElementById('signatureComments').value;
|
||||
|
||||
// Close the modal
|
||||
const signatureModal = bootstrap.Modal.getInstance(document.getElementById('signatureModal'));
|
||||
signatureModal.hide();
|
||||
|
||||
// Show success message
|
||||
alert('Contract signed successfully!');
|
||||
|
||||
// Reload the page to show the updated contract
|
||||
// In a real app, we would update the UI without reloading
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<p class="mb-2"><strong>Error Message:</strong></p>
|
||||
<pre class="p-3 bg-light border rounded"><code>{{ error | default(value="Unknown error") }}</code></pre>
|
||||
<pre class="p-3 bg-light border rounded"><code>{% if error %}{{ error }}{% else %}Unknown error{% endif %}</code></pre>
|
||||
</div>
|
||||
|
||||
{% if error_details is defined and error_details %}
|
||||
|
10
actix_mvc_app/src/views/test.html
Normal file
10
actix_mvc_app/src/views/test.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page</h1>
|
||||
<p>This is a simple test page to verify template rendering.</p>
|
||||
</body>
|
||||
</html>
|
10
actix_mvc_app/src/views/test_base.html
Normal file
10
actix_mvc_app/src/views/test_base.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Test Base Template{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Test Base Template</h1>
|
||||
<p>This is a simplified template for testing that extends base.html.</p>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user