Compare commits

19 Commits

Author SHA1 Message Date
timurgordon
8c2276de79 flowbroker wip 2025-05-27 11:42:52 +03:00
timurgordon
123dfc606c multisig rhai flow POC app 2025-05-20 22:08:00 +03:00
795c04fc5a Merge pull request 'development_timur' (#1) from development_timur into main
Reviewed-on: #1
2025-05-19 11:51:37 +00:00
timurgordon
2cfec627bf improve registration view 2025-05-19 14:49:06 +03:00
timurgordon
83dde53555 implement signature requests over ws 2025-05-19 14:48:40 +03:00
timurgordon
2fd74defab update governance ui 2025-05-16 14:07:20 +03:00
timurgordon
9468595395 Add company management module with registration and entity switching 2025-05-05 13:58:51 +03:00
timurgordon
2760f00a30 Vocabulary fixes 2025-05-05 11:32:09 +03:00
timurgordon
a7c0772d9b fix images 2025-05-05 11:12:15 +03:00
timurgordon
54762cb63f vocabulary change 2025-05-05 10:49:33 +03:00
timurgordon
bafb63e0b1 Merge branch 'development_timur' of https://git.ourworld.tf/herocode/hostbasket into development_timur 2025-05-01 12:15:14 +03:00
timurgordon
c05803ff58 add contract md folder support 2025-05-01 03:56:55 +03:00
Timur Gordon
6b7b2542ab move all defi functionality to page and separate controller 2025-05-01 02:55:41 +02:00
457f3c8268 fixes 2025-04-29 06:27:28 +04:00
Timur Gordon
19f8700b78 feat: Implement comprehensive DeFi platform in Digital Assets dashboard
Add a complete DeFi platform with the following features:
- Tabbed interface for different DeFi functionalities
- Lending & Borrowing system with APY calculations
- Liquidity Pools with LP token rewards
- Staking options for tokens and digital assets
- Token Swap interface with real-time exchange rates
- Collateralization system for loans and synthetic assets
- Interactive JavaScript functionality for real-time calculations

This enhancement provides users with a complete suite of DeFi tools
directly integrated into the Digital Assets dashboard.
2025-04-29 01:11:51 +02:00
Timur Gordon
c22d6c953e implement marketplace feature wip 2025-04-26 03:44:36 +02:00
Timur Gordon
9445dea629 styling and minor content fixes 2025-04-23 04:58:38 +02:00
Timur Gordon
b56f1cbc30 updates to mock content and contract view implementation 2025-04-23 03:52:11 +02:00
Timur Gordon
6060831f61 rwda ui implementation 2025-04-22 15:36:40 +02:00
119 changed files with 29606 additions and 769 deletions

193
actix_mvc_app/Cargo.lock generated
View File

@@ -107,6 +107,45 @@ dependencies = [
"syn",
]
[[package]]
name = "actix-multipart"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d974dd6c4f78d102d057c672dcf6faa618fafa9df91d44f9c466688fc1275a3a"
dependencies = [
"actix-multipart-derive",
"actix-utils",
"actix-web",
"bytes",
"derive_more 0.99.19",
"futures-core",
"futures-util",
"httparse",
"local-waker",
"log",
"memchr",
"mime",
"rand 0.8.5",
"serde",
"serde_json",
"serde_plain",
"tempfile",
"tokio",
]
[[package]]
name = "actix-multipart-derive"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
dependencies = [
"darling",
"parse-size",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "actix-router"
version = "0.5.3"
@@ -247,6 +286,7 @@ version = "0.1.0"
dependencies = [
"actix-files",
"actix-identity",
"actix-multipart",
"actix-session",
"actix-web",
"bcrypt",
@@ -255,14 +295,17 @@ dependencies = [
"dotenv",
"env_logger",
"futures",
"futures-util",
"jsonwebtoken",
"lazy_static",
"log",
"num_cpus",
"pulldown-cmark",
"redis",
"serde",
"serde_json",
"tera",
"urlencoding",
"uuid",
]
@@ -821,6 +864,41 @@ dependencies = [
"cipher",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.4.0"
@@ -945,6 +1023,22 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "flate2"
version = "1.1.1"
@@ -1075,6 +1169,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -1386,6 +1489,12 @@ dependencies = [
"syn",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
@@ -1553,6 +1662,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "litemap"
version = "0.7.5"
@@ -1749,6 +1864,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
@@ -1931,6 +2052,25 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
dependencies = [
"bitflags",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quote"
version = "1.0.40"
@@ -2122,6 +2262,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
version = "1.0.20"
@@ -2187,6 +2340,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
@@ -2326,6 +2488,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
@@ -2354,6 +2522,19 @@ dependencies = [
"syn",
]
[[package]]
name = "tempfile"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "tera"
version = "1.20.0"
@@ -2622,6 +2803,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@@ -2655,6 +2842,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf16_iter"
version = "1.0.5"

View File

@@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
actix-multipart = "0.6.1"
futures-util = "0.3.30"
actix-web = "4.5.1"
actix-files = "0.6.5"
tera = "1.19.1"
@@ -23,3 +25,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] }
lazy_static = "1.4.0"
redis = { version = "0.23.0", features = ["tokio-comp"] }
jsonwebtoken = "8.3.0"
pulldown-cmark = "0.13.0"
urlencoding = "2.1.3"

View File

@@ -1,4 +1,4 @@
# Zanzibar Autonomous Zone
# Zanzibar Digital Freezone
Convenience, Safety and Privacy
@@ -42,8 +42,8 @@ actix_mvc_app/
1. Clone the repository:
```
git clone https://github.com/yourusername/zanzibar-autonomous-zone.git
cd zanzibar-autonomous-zone
git clone https://github.com/yourusername/zanzibar-digital-freezone.git
cd zanzibar-digital-freezone
```
2. Build the project:

View File

@@ -0,0 +1,3 @@
## 1. Purpose
The purpose of this Agreement is to establish the terms and conditions for tokenizing real estate assets on the Zanzibar blockchain network.

View File

@@ -0,0 +1,3 @@
## 2. Tokenization Process
Tokenizer shall create digital tokens representing ownership interests in the properties listed in Appendix A according to the specifications in Appendix B.

View File

@@ -0,0 +1,3 @@
## 3. Revenue Sharing
Revenue generated from the tokenized properties shall be distributed according to the formula set forth in Appendix C.

View File

@@ -0,0 +1,3 @@
## 4. Governance
Decisions regarding the management of tokenized properties shall be made according to the governance framework outlined in Appendix D.

View File

@@ -0,0 +1,3 @@
### Appendix A: Properties
List of properties to be tokenized.

View File

@@ -0,0 +1,3 @@
### Appendix B: Specifications
Technical specifications for tokenization.

View File

@@ -0,0 +1,3 @@
### Appendix C: Revenue Formula
Formula for revenue distribution.

View File

@@ -0,0 +1,3 @@
### Appendix D: Governance Framework
Governance framework for tokenized properties.

View File

@@ -0,0 +1,3 @@
# Digital Asset Tokenization Agreement
This Digital Asset Tokenization Agreement (the "Agreement") is entered into between Zanzibar Property Consortium ("Tokenizer") and the property owners listed in Appendix A ("Owners").

View 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::Artwork,
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![
("Artwork", "Artwork"),
("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
pub 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(750000.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": 75.0
}),
image_url: Some("https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=600&q=80".to_string()),
external_url: Some("https://oceanviewholdings.zdfz/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(650000.0, "USD", "ZDFZ Property Registry", Some("Initial tokenization valuation".to_string()));
zanzibar_resort.add_valuation(700000.0, "USD", "International Property Appraisers", Some("Independent third-party valuation".to_string()));
zanzibar_resort.add_valuation(750000.0, "USD", "ZDFZ Property Registry", Some("Updated valuation after infrastructure improvements".to_string()));
zanzibar_resort.add_transaction(
"Tokenization",
None,
Some("0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string()),
Some(650000.0),
Some("USD".to_string()),
Some("0xabcdef123456789abcdef123456789abcdef123456789abcdef123456789abcd".to_string()),
Some("Initial property tokenization under ZDFZ Property Registry".to_string()),
);
zanzibar_resort.add_transaction(
"Token Sale",
Some("0xc3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".to_string()),
Some("0x7a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
Some(75000.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 ZDFZ Governance Token
let mut zaz_token = Asset {
id: "asset-zdfz-governance".to_string(),
name: "ZDFZ Governance Token".to_string(),
description: "Official governance token of the Zanzibar Digital Freezone, used for voting on proposals and zone-wide decisions".to_string(),
asset_type: AssetType::Token,
status: AssetStatus::Active,
owner_id: "entity-zdfz-foundation".to_string(),
owner_name: "Zanzibar Digital Freezone Foundation".to_string(),
created_at: now - Duration::days(365),
updated_at: now - Duration::days(2),
blockchain_info: None,
current_valuation: Some(350000.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://images.unsplash.com/photo-1431540015161-0bf868a2d407?q=80&w=3540&?auto=format&fit=crop&w=600&q=80".to_string()),
external_url: Some("https://governance.zdfz/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(300000.0, "USD", "ZDFZ Token Exchange", Some("Initial valuation at launch".to_string()));
zaz_token.add_valuation(320000.0, "USD", "ZDFZ Token Exchange", Some("Valuation after successful governance implementation".to_string()));
zaz_token.add_valuation(350000.0, "USD", "ZDFZ Token Exchange", Some("Current market valuation".to_string()));
zaz_token.add_transaction(
"Distribution",
Some("0xe5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6".to_string()),
Some("0x9a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
Some(300000.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(2000000.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(200000.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://images.unsplash.com/photo-1464983953574-0892a716854b?auto=format&fit=crop&w=600&q=80".to_string()),
external_url: Some("https://spicetrade.zdfz".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(150000.0, "USD", "ZDFZ Business Registry", Some("Initial company valuation at incorporation".to_string()));
spice_trade_shares.add_valuation(175000.0, "USD", "ZDFZ Business Registry", Some("Valuation after first export contracts".to_string()));
spice_trade_shares.add_valuation(200000.0, "USD", "ZDFZ Business Registry", Some("Current valuation after expansion to European markets".to_string()));
spice_trade_shares.add_transaction(
"Share Issuance",
None,
Some("0x6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b".to_string()),
Some(150000.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(50000.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-zdfz-energy-innovations".to_string(),
owner_name: "ZDFZ Energy Innovations".to_string(),
created_at: now - Duration::days(210),
updated_at: now - Duration::days(30),
blockchain_info: None,
current_valuation: Some(120000.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": "ZDFZ-PAT-2024-0142",
"filing_date": (now - Duration::days(210)).to_rfc3339(),
"grant_date": (now - Duration::days(120)).to_rfc3339(),
"patent_type": "Utility",
"jurisdiction": "Zanzibar Digital Freezone",
"inventors": ["Dr. Amina Juma", "Eng. Ibrahim Hassan", "Dr. Sarah Mbeki"]
}),
image_url: Some("https://images.unsplash.com/photo-1708851148146-783a5b7da55d?q=80&w=3474&?auto=format&fit=crop&w=600&q=80".to_string()),
external_url: Some("https://patents.zdfz/ZDFZ-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(80000.0, "USD", "ZDFZ IP Registry", Some("Initial patent valuation upon filing".to_string()));
tidal_energy_patent.add_valuation(100000.0, "USD", "ZDFZ IP Registry", Some("Valuation after successful prototype testing".to_string()));
tidal_energy_patent.add_valuation(120000.0, "USD", "ZDFZ IP Registry", Some("Current valuation after pilot implementation".to_string()));
tidal_energy_patent.add_transaction(
"Registration",
None,
Some("0x4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f".to_string()),
Some(80000.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(20000.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 Artwork
let mut zanzibar_heritage_nft = Asset {
id: "asset-heritage-Artwork".to_string(),
name: "Zanzibar Heritage Collection #1".to_string(),
description: "Limited edition digital art Artwork showcasing Zanzibar's cultural heritage, created by renowned local artist Fatma Busaidy".to_string(),
asset_type: AssetType::Artwork,
status: AssetStatus::Active,
owner_id: "entity-zdfz-digital-arts".to_string(),
owner_name: "ZDFZ Digital Arts Collective".to_string(),
created_at: now - Duration::days(90),
updated_at: now - Duration::days(10),
blockchain_info: None,
current_valuation: Some(6000.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": "ZDFZ-ART-CERT-2024-089"
}),
image_url: Some("https://images.unsplash.com/photo-1519125323398-675f0ddb6308?auto=format&fit=crop&w=600&q=80".to_string()),
external_url: Some("https://digitalarts.zdfz/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(5000.0, "USD", "ZDFZ Artwork Marketplace", Some("Initial offering price".to_string()));
zanzibar_heritage_nft.add_valuation(5500.0, "USD", "ZDFZ Artwork Marketplace", Some("Valuation after artist exhibition".to_string()));
zanzibar_heritage_nft.add_valuation(6000.0, "USD", "ZDFZ Artwork Marketplace", Some("Current market valuation".to_string()));
zanzibar_heritage_nft.add_transaction(
"Minting",
None,
Some("0xb794f5ea0ba39494ce839613fffba74279579268".to_string()),
Some(0.0),
Some("ETH".to_string()),
Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
Some("Initial Artwork minting by artist".to_string()),
);
zanzibar_heritage_nft.add_transaction(
"Sale",
Some("0xb794f5ea0ba39494ce839613fffba74279579268".to_string()),
Some("0xa1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".to_string()),
Some(5000.0),
Some("USD".to_string()),
Some("0x234567890abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string()),
Some("Primary sale to ZDFZ Digital Arts Collective".to_string()),
);
assets.push(zanzibar_heritage_nft);
assets
}
}

View File

@@ -0,0 +1,245 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_web::HttpRequest;
use tera::{Context, Tera};
use serde::Deserialize;
use chrono::Utc;
use crate::utils::render_template;
// Form structs for company operations
#[derive(Debug, Deserialize)]
pub struct CompanyRegistrationForm {
pub company_name: String,
pub company_type: String,
pub shareholders: String,
pub company_purpose: Option<String>,
}
pub struct CompanyController;
impl CompanyController {
// Display the company management dashboard
pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
let mut context = Context::new();
println!("DEBUG: Starting Company dashboard rendering");
// Add active_page for navigation highlighting
context.insert("active_page", &"company");
// Parse query parameters
let query_string = req.query_string();
// Check for success message
if let Some(pos) = query_string.find("success=") {
let start = pos + 8; // length of "success="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let success = &query_string[start..end];
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
context.insert("success", &decoded);
}
// Check for entity context
if let Some(pos) = query_string.find("entity=") {
let start = pos + 7; // length of "entity="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let entity = &query_string[start..end];
context.insert("entity", &entity);
// Also get entity name if present
if let Some(pos) = query_string.find("entity_name=") {
let start = pos + 12; // length of "entity_name="
let end = query_string[start..].find('&').map_or(query_string.len(), |e| e + start);
let entity_name = &query_string[start..end];
let decoded_name = urlencoding::decode(entity_name).unwrap_or_else(|_| entity_name.into());
context.insert("entity_name", &decoded_name);
println!("DEBUG: Entity context set to {} ({})", entity, decoded_name);
}
}
println!("DEBUG: Rendering Company dashboard template");
let response = render_template(&tmpl, "company/index.html", &context);
println!("DEBUG: Finished rendering Company dashboard template");
response
}
// View company details
pub async fn view_company(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
let company_id = path.into_inner();
let mut context = Context::new();
println!("DEBUG: Viewing company details for {}", company_id);
// Add active_page for navigation highlighting
context.insert("active_page", &"company");
context.insert("company_id", &company_id);
// In a real application, we would fetch company data from a database
// For now, we'll use mock data based on the company_id
match company_id.as_str() {
"company1" => {
context.insert("company_name", &"Zanzibar Digital Solutions");
context.insert("company_type", &"Startup FZC");
context.insert("status", &"Active");
context.insert("registration_date", &"2025-04-01");
context.insert("purpose", &"Digital solutions and blockchain development");
context.insert("plan", &"Startup FZC - $50/month");
context.insert("next_billing", &"2025-06-01");
context.insert("payment_method", &"Credit Card (****4582)");
// Shareholders data
let shareholders = vec![
("John Smith", "60%"),
("Sarah Johnson", "40%"),
];
context.insert("shareholders", &shareholders);
// Contracts data
let contracts = vec![
("Articles of Incorporation", "Signed"),
("Terms & Conditions", "Signed"),
("Digital Asset Issuance", "Signed"),
];
context.insert("contracts", &contracts);
},
"company2" => {
context.insert("company_name", &"Blockchain Innovations Ltd");
context.insert("company_type", &"Growth FZC");
context.insert("status", &"Active");
context.insert("registration_date", &"2025-03-15");
context.insert("purpose", &"Blockchain technology research and development");
context.insert("plan", &"Growth FZC - $100/month");
context.insert("next_billing", &"2025-06-15");
context.insert("payment_method", &"Bank Transfer");
// Shareholders data
let shareholders = vec![
("Michael Chen", "35%"),
("Aisha Patel", "35%"),
("David Okonkwo", "30%"),
];
context.insert("shareholders", &shareholders);
// Contracts data
let contracts = vec![
("Articles of Incorporation", "Signed"),
("Terms & Conditions", "Signed"),
("Digital Asset Issuance", "Signed"),
("Physical Asset Holding", "Signed"),
];
context.insert("contracts", &contracts);
},
"company3" => {
context.insert("company_name", &"Sustainable Energy Cooperative");
context.insert("company_type", &"Cooperative FZC");
context.insert("status", &"Pending");
context.insert("registration_date", &"2025-05-01");
context.insert("purpose", &"Renewable energy production and distribution");
context.insert("plan", &"Cooperative FZC - $200/month");
context.insert("next_billing", &"Pending Activation");
context.insert("payment_method", &"Pending");
// Shareholders data
let shareholders = vec![
("Community Energy Group", "40%"),
("Green Future Initiative", "30%"),
("Sustainable Living Collective", "30%"),
];
context.insert("shareholders", &shareholders);
// Contracts data
let contracts = vec![
("Articles of Incorporation", "Signed"),
("Terms & Conditions", "Signed"),
("Cooperative Governance", "Pending"),
];
context.insert("contracts", &contracts);
},
_ => {
// If company_id is not recognized, redirect to company index
return Ok(HttpResponse::Found()
.append_header(("Location", "/company"))
.finish());
}
}
println!("DEBUG: Rendering company view template");
let response = render_template(&tmpl, "company/view.html", &context);
println!("DEBUG: Finished rendering company view template");
response
}
// Switch to entity context
pub async fn switch_entity(path: web::Path<String>) -> Result<HttpResponse> {
let company_id = path.into_inner();
println!("DEBUG: Switching to entity context for {}", company_id);
// Get company name based on ID (in a real app, this would come from a database)
let company_name = match company_id.as_str() {
"company1" => "Zanzibar Digital Solutions",
"company2" => "Blockchain Innovations Ltd",
"company3" => "Sustainable Energy Cooperative",
_ => "Unknown Company"
};
// In a real application, we would set a session/cookie for the current entity
// Here we'll redirect back to the company page with a success message and entity parameter
let success_message = format!("Switched to {} entity context", company_name);
let encoded_message = urlencoding::encode(&success_message);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/company?success={}&entity={}&entity_name={}",
encoded_message, company_id, urlencoding::encode(company_name))))
.finish())
}
// Process company registration
pub async fn register(
mut form: actix_multipart::Multipart,
) -> Result<HttpResponse> {
use actix_web::{http::header};
use futures_util::stream::StreamExt as _;
use std::collections::HashMap;
println!("DEBUG: Processing company registration request");
let mut fields: HashMap<String, String> = HashMap::new();
let mut files = Vec::new();
// Parse multipart form
while let Some(Ok(mut field)) = form.next().await {
let mut value = Vec::new();
while let Some(chunk) = field.next().await {
let data = chunk.unwrap();
value.extend_from_slice(&data);
}
// Get field name from content disposition
let cd = field.content_disposition();
if let Some(name) = cd.get_name() {
if name == "company_docs" {
files.push(value); // Just collect files in memory for now
} else {
fields.insert(name.to_string(), String::from_utf8_lossy(&value).to_string());
}
}
}
// Extract company details
let company_name = fields.get("company_name").cloned().unwrap_or_default();
let company_type = fields.get("company_type").cloned().unwrap_or_default();
let shareholders = fields.get("shareholders").cloned().unwrap_or_default();
// Log received fields (mock DB insert)
println!("[Company Registration] Name: {}, Type: {}, Shareholders: {}, Files: {}",
company_name, company_type, shareholders, files.len());
// Create success message
let success_message = format!("Successfully registered {} as a {}", company_name, company_type);
// Redirect back to /company with success message
Ok(HttpResponse::SeeOther()
.append_header((header::LOCATION, format!("/company?success={}", urlencoding::encode(&success_message))))
.finish())
}
}

View File

@@ -1,9 +1,12 @@
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 actix_web::web::Query;
use std::collections::HashMap;
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, SignerStatus};
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, TocItem};
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
@@ -24,7 +27,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 +70,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 +89,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 +107,11 @@ 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>,
query: Query<HashMap<String, String>>
) -> Result<HttpResponse, Error> {
let contract_id = path.into_inner();
let mut context = Context::new();
@@ -113,26 +120,83 @@ 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);
// If this contract uses multi-page markdown, load the selected section
println!("DEBUG: content_dir = {:?}, toc = {:?}", contract.content_dir, contract.toc);
if let (Some(content_dir), Some(toc)) = (&contract.content_dir, &contract.toc) {
use std::fs;
use pulldown_cmark::{Parser, Options, html};
// Helper to flatten toc recursively
fn flatten_toc<'a>(items: &'a Vec<TocItem>, out: &mut Vec<&'a TocItem>) {
for item in items {
out.push(item);
if !item.children.is_empty() {
flatten_toc(&item.children, out);
}
}
}
let mut flat_toc = Vec::new();
flatten_toc(&toc, &mut flat_toc);
let section_param = query.get("section");
let selected_file = section_param
.and_then(|f| flat_toc.iter().find(|item| item.file == *f).map(|item| item.file.clone()))
.unwrap_or_else(|| flat_toc.get(0).map(|item| item.file.clone()).unwrap_or_default());
context.insert("section", &selected_file);
let rel_path = format!("{}/{}", content_dir, selected_file);
let abs_path = match std::env::current_dir() {
Ok(dir) => dir.join(&rel_path),
Err(_) => std::path::PathBuf::from(&rel_path),
};
println!("DEBUG: Attempting to read markdown file at absolute path: {:?}", abs_path);
match fs::read_to_string(&abs_path) {
Ok(md) => {
println!("DEBUG: Successfully read markdown file");
let parser = Parser::new_ext(&md, Options::all());
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
context.insert("contract_section_content", &html_output);
},
Err(e) => {
let error_msg = format!("Error: Could not read contract section markdown at '{:?}': {}", abs_path, e);
println!("{}", error_msg);
context.insert("contract_section_content_error", &error_msg);
}
}
context.insert("toc", &toc);
}
// 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 +220,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 +231,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 +255,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 +280,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 +301,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 +373,369 @@ 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 {
content_dir: None,
toc: None,
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 {
content_dir: None,
toc: None,
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,
content_dir: Some("src/content/contract-003".to_string()),
toc: Some(vec![
TocItem {
title: "Cover".to_string(),
file: "cover.md".to_string(),
children: vec![],
},
TocItem {
title: "1. Purpose".to_string(),
file: "1-purpose.md".to_string(),
children: vec![],
},
TocItem {
title: "2. Tokenization Process".to_string(),
file: "2-tokenization-process.md".to_string(),
children: vec![],
},
TocItem {
title: "3. Revenue Sharing".to_string(),
file: "3-revenue-sharing.md".to_string(),
children: vec![],
},
TocItem {
title: "4. Governance".to_string(),
file: "4-governance.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix A: Properties".to_string(),
file: "appendix-a.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix B: Technical Specs".to_string(),
file: "appendix-b.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix C: Revenue Formula".to_string(),
file: "appendix-c.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix D: Governance Framework".to_string(),
file: "appendix-d.md".to_string(),
children: vec![],
},
]),
};
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 ToC and content directory to contract 3
contract3.content_dir = Some("src/content/contract-003".to_string());
contract3.toc = Some(vec![
TocItem {
title: "Digital Asset Tokenization Agreement".to_string(),
file: "cover.md".to_string(),
children: vec![
TocItem {
title: "1. Purpose".to_string(),
file: "1-purpose.md".to_string(),
children: vec![],
},
TocItem {
title: "2. Tokenization Process".to_string(),
file: "2-tokenization-process.md".to_string(),
children: vec![],
},
TocItem {
title: "3. Revenue Sharing".to_string(),
file: "3-revenue-sharing.md".to_string(),
children: vec![],
},
TocItem {
title: "4. Governance".to_string(),
file: "4-governance.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix A: Properties".to_string(),
file: "appendix-a.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix B: Specifications".to_string(),
file: "appendix-b.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix C: Revenue Formula".to_string(),
file: "appendix-c.md".to_string(),
children: vec![],
},
TocItem {
title: "Appendix D: Governance Framework".to_string(),
file: "appendix-d.md".to_string(),
children: vec![],
},
],
}
]);
// No revision content for contract 3, content is in markdown files.
// Mark both signers as signed
for signer in contract3.signers.iter_mut() {
signer.sign(None);
}
// Mock contract 4 - Rejected
let mut contract4 = Contract {
content_dir: None,
toc: None,
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 {
content_dir: None,
toc: None,
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 Digital Freezone.".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
}

View File

@@ -0,0 +1,368 @@
use actix_web::{web, HttpResponse, Result};
use actix_web::HttpRequest;
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use serde::Deserialize;
use uuid::Uuid;
use crate::models::asset::{Asset, AssetType, AssetStatus};
use crate::models::defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB};
use crate::utils::render_template;
// Form structs for DeFi operations
#[derive(Debug, Deserialize)]
pub struct ProvidingForm {
pub asset_id: String,
pub amount: f64,
pub duration: i32,
}
#[derive(Debug, Deserialize)]
pub struct ReceivingForm {
pub collateral_asset_id: String,
pub collateral_amount: f64,
pub amount: f64,
pub duration: i32,
}
#[derive(Debug, Deserialize)]
pub struct LiquidityForm {
pub first_token: String,
pub first_amount: f64,
pub second_token: String,
pub second_amount: f64,
pub pool_fee: f64,
}
#[derive(Debug, Deserialize)]
pub struct StakingForm {
pub asset_id: String,
pub amount: f64,
pub staking_period: i32,
}
#[derive(Debug, Deserialize)]
pub struct SwapForm {
pub from_token: String,
pub from_amount: f64,
pub to_token: String,
}
#[derive(Debug, Deserialize)]
pub struct CollateralForm {
pub asset_id: String,
pub amount: f64,
pub purpose: String,
pub funds_amount: Option<f64>,
pub funds_term: Option<i32>,
}
pub struct DefiController;
impl DefiController {
// Display the DeFi dashboard
pub async fn index(tmpl: web::Data<Tera>, req: HttpRequest) -> Result<HttpResponse> {
let mut context = Context::new();
println!("DEBUG: Starting DeFi dashboard rendering");
// Get mock assets for the dropdown selectors
let assets = Self::get_mock_assets();
println!("DEBUG: Generated {} mock assets", assets.len());
// Add active_page for navigation highlighting
context.insert("active_page", &"defi");
// Add DeFi stats
let defi_stats = Self::get_defi_stats();
context.insert("defi_stats", &serde_json::to_value(defi_stats).unwrap());
// Add recent assets for selection in forms
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);
// Get user's providing positions
let db = DEFI_DB.lock().unwrap();
let providing_positions = db.get_user_providing_positions("user123");
let providing_positions_json: Vec<serde_json::Value> = providing_positions
.iter()
.map(|p| serde_json::to_value(p).unwrap())
.collect();
context.insert("providing_positions", &providing_positions_json);
// Get user's receiving positions
let receiving_positions = db.get_user_receiving_positions("user123");
let receiving_positions_json: Vec<serde_json::Value> = receiving_positions
.iter()
.map(|p| serde_json::to_value(p).unwrap())
.collect();
context.insert("receiving_positions", &receiving_positions_json);
// Add success message if present in query params
if let Some(success) = req.query_string().strip_prefix("success=") {
let decoded = urlencoding::decode(success).unwrap_or_else(|_| success.into());
context.insert("success_message", &decoded);
}
println!("DEBUG: Rendering DeFi dashboard template");
let response = render_template(&tmpl, "defi/index.html", &context);
println!("DEBUG: Finished rendering DeFi dashboard template");
response
}
// Process providing request
pub async fn create_providing(_tmpl: web::Data<Tera>, form: web::Form<ProvidingForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing providing request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database)
let assets = Self::get_mock_assets();
let asset = assets.iter().find(|a| a.id == form.asset_id);
if let Some(asset) = asset {
// Calculate profit share and return amount
let profit_share = match form.duration {
7 => 2.5,
30 => 4.2,
90 => 6.8,
180 => 8.5,
365 => 12.0,
_ => 4.2, // Default to 30 days rate
};
let return_amount = form.amount + (form.amount * (profit_share / 100.0) * (form.duration as f64 / 365.0));
// Create a new providing position
let providing_position = ProvidingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Providing,
status: DefiPositionStatus::Active,
asset_id: form.asset_id.clone(),
asset_name: asset.name.clone(),
asset_symbol: asset.asset_type.as_str().to_string(),
amount: form.amount,
value_usd: form.amount * asset.current_valuation.unwrap_or(0.0),
expected_return: profit_share,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::days(form.duration as i64)),
user_id: "user123".to_string(), // Hardcoded user ID for now
},
duration_days: form.duration,
profit_share_earned: profit_share,
return_amount,
};
// Add the position to the database
{
let mut db = DEFI_DB.lock().unwrap();
db.add_providing_position(providing_position);
}
// Redirect with success message
let success_message = format!("Successfully provided {} {} for {} days", form.amount, asset.name, form.duration);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
} else {
// Asset not found, redirect with error
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/defi?error=Asset not found"))
.finish())
}
}
// Process receiving request
pub async fn create_receiving(_tmpl: web::Data<Tera>, form: web::Form<ReceivingForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing receiving request: {:?}", form);
// Get the asset obligationails (in a real app, this would come from a database)
let assets = Self::get_mock_assets();
let collateral_asset = assets.iter().find(|a| a.id == form.collateral_asset_id);
if let Some(collateral_asset) = collateral_asset {
// Calculate profit share rate based on duration
let profit_share_rate = match form.duration {
7 => 3.5,
30 => 5.0,
90 => 6.5,
180 => 8.0,
365 => 10.0,
_ => 5.0, // Default to 30 days rate
};
// Calculate profit share and total to repay
let profit_share = form.amount * (profit_share_rate / 100.0) * (form.duration as f64 / 365.0);
let total_to_repay = form.amount + profit_share;
// Calculate collateral value and ratio
let collateral_value = form.collateral_amount * collateral_asset.latest_valuation().map_or(0.5, |v| v.value);
let collateral_ratio = (collateral_value / form.amount) * 100.0;
// Create a new receiving position
let receiving_position = ReceivingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Receiving,
status: DefiPositionStatus::Active,
asset_id: "ZDFZ".to_string(), // Hardcoded for now, in a real app this would be a parameter
asset_name: "Zanzibar Token".to_string(),
asset_symbol: "ZDFZ".to_string(),
amount: form.amount,
value_usd: form.amount * 0.5, // Assuming 0.5 USD per ZDFZ
expected_return: profit_share_rate,
created_at: Utc::now(),
expires_at: Some(Utc::now() + Duration::days(form.duration as i64)),
user_id: "user123".to_string(), // Hardcoded user ID for now
},
collateral_asset_id: collateral_asset.id.clone(),
collateral_asset_name: collateral_asset.name.clone(),
collateral_asset_symbol: collateral_asset.asset_type.as_str().to_string(),
collateral_amount: form.collateral_amount,
collateral_value_usd: collateral_value,
duration_days: form.duration,
profit_share_rate,
profit_share_owed: profit_share,
total_to_repay,
collateral_ratio,
};
// Add the position to the database
{
let mut db = DEFI_DB.lock().unwrap();
db.add_receiving_position(receiving_position);
}
// Redirect with success message
let success_message = format!("Successfully borrowed {} ZDFZ using {} {} as collateral",
form.amount, form.collateral_amount, collateral_asset.name);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
} else {
// Asset not found, redirect with error
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/defi?error=Collateral asset not found"))
.finish())
}
}
// Process liquidity provision
pub async fn add_liquidity(_tmpl: web::Data<Tera>, form: web::Form<LiquidityForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing liquidity provision: {:?}", form);
// In a real application, this would add liquidity to a pool in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully added liquidity: {} {} and {} {}",
form.first_amount, form.first_token, form.second_amount, form.second_token);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
}
// Process staking request
pub async fn create_staking(_tmpl: web::Data<Tera>, form: web::Form<StakingForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing staking request: {:?}", form);
// In a real application, this would create a staking position in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully staked {} {}", form.amount, form.asset_id);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
}
// Process token swap
pub async fn swap_tokens(_tmpl: web::Data<Tera>, form: web::Form<SwapForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing token swap: {:?}", form);
// In a real application, this would perform a token swap in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let success_message = format!("Successfully swapped {} {} to {}",
form.from_amount, form.from_token, form.to_token);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
}
// Process collateral position creation
pub async fn create_collateral(_tmpl: web::Data<Tera>, form: web::Form<CollateralForm>) -> Result<HttpResponse> {
println!("DEBUG: Processing collateral creation: {:?}", form);
// In a real application, this would create a collateral position in the database
// For now, we'll just redirect back to the DeFi dashboard with a success message
let purpose_str = match form.purpose.as_str() {
"funds" => "secure a funds",
"synthetic" => "generate synthetic assets",
"leverage" => "leverage trading",
_ => "collateralization",
};
let success_message = format!("Successfully collateralized {} {} for {}",
form.amount, form.asset_id, purpose_str);
Ok(HttpResponse::SeeOther()
.append_header(("Location", format!("/defi?success={}", urlencoding::encode(&success_message))))
.finish())
}
// Helper method to get DeFi statistics
fn get_defi_stats() -> serde_json::Map<String, serde_json::Value> {
let mut stats = serde_json::Map::new();
// Handle Option<Number> by unwrapping with expect
stats.insert("total_value_locked".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(1250000.0).expect("Valid float")));
stats.insert("providing_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(450000.0).expect("Valid float")));
stats.insert("receiving_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(320000.0).expect("Valid float")));
stats.insert("liquidity_pools_count".to_string(), serde_json::Value::Number(serde_json::Number::from(12)));
stats.insert("active_stakers".to_string(), serde_json::Value::Number(serde_json::Number::from(156)));
stats.insert("total_swap_volume".to_string(), serde_json::Value::Number(serde_json::Number::from_f64(780000.0).expect("Valid float")));
stats
}
// 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()));
// Add current valuation
if let Some(latest) = asset.latest_valuation() {
if let Some(num) = serde_json::Number::from_f64(latest.value) {
map.insert("current_valuation".to_string(), serde_json::Value::Number(num));
} else {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
}
map.insert("valuation_currency".to_string(), serde_json::Value::String(latest.currency.clone()));
map.insert("valuation_date".to_string(), serde_json::Value::String(latest.date.format("%Y-%m-%d").to_string()));
} else {
map.insert("current_valuation".to_string(), serde_json::Value::Number(serde_json::Number::from(0)));
map.insert("valuation_currency".to_string(), serde_json::Value::String("USD".to_string()));
map.insert("valuation_date".to_string(), serde_json::Value::String("N/A".to_string()));
}
map
}
// Generate mock assets for testing
fn get_mock_assets() -> Vec<Asset> {
// Reuse the asset controller's mock data function
crate::controllers::asset::AssetController::get_mock_assets()
}
}

View File

@@ -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: "ZDFZ Business Entity Registration".to_string(),
description: "Register a new business entity within the Zanzibar Digital Freezone 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: "ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ-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 ZDFZ 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 ZDFZ Financial Authority".to_string(),
timestamp: Utc::now() - Duration::days(12),
},
FlowLog {
id: "log-2-4-3".to_string(),
message: "Asset tokens listed on ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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 ZDFZ 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
}

View File

@@ -12,9 +12,24 @@ pub struct GovernanceController;
impl GovernanceController {
/// Helper function to get user from session
/// For testing purposes, this will always return a mock user
fn get_user_from_session(session: &Session) -> Option<Value> {
session.get::<String>("user").ok().flatten().and_then(|user_json| {
// Try to get user from session first
let session_user = session.get::<String>("user").ok().flatten().and_then(|user_json| {
serde_json::from_str(&user_json).ok()
});
// If user is not in session, return a mock user for testing
session_user.or_else(|| {
// Create a mock user
let mock_user = serde_json::json!({
"id": 1,
"username": "test_user",
"email": "test@example.com",
"name": "Test User",
"role": "member"
});
Some(mock_user)
})
}
@@ -23,14 +38,32 @@ impl GovernanceController {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get mock proposals for the dashboard
let proposals = Self::get_mock_proposals();
ctx.insert("proposals", &proposals);
let mut proposals = Self::get_mock_proposals();
// Filter for active proposals only
let active_proposals: Vec<Proposal> = proposals.into_iter()
.filter(|p| p.status == ProposalStatus::Active)
.collect();
// Sort active proposals by voting end date (ascending)
let mut sorted_active_proposals = active_proposals.clone();
sorted_active_proposals.sort_by(|a, b| a.voting_ends_at.cmp(&b.voting_ends_at));
ctx.insert("proposals", &sorted_active_proposals);
// Get the nearest deadline proposal for the voting pane
if let Some(nearest_proposal) = sorted_active_proposals.first() {
ctx.insert("nearest_proposal", nearest_proposal);
}
// Get recent activity for the timeline
let recent_activity = Self::get_mock_recent_activity();
ctx.insert("recent_activity", &recent_activity);
// Get some statistics
let stats = Self::get_mock_statistics();
@@ -106,13 +139,9 @@ impl GovernanceController {
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "create");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
} else {
// Redirect to login if not logged in
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
render_template(&tmpl, "governance/create_proposal.html", &ctx)
}
@@ -123,18 +152,12 @@ impl GovernanceController {
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
// Check if user is logged in
if Self::get_user_from_session(&session).is_none() {
return Ok(HttpResponse::Found().append_header(("Location", "/login")).finish());
}
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// In a real application, we would save the proposal to a database
// For now, we'll just redirect to the proposals page with a success message
@@ -204,19 +227,77 @@ impl GovernanceController {
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "my_votes");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
// Get mock votes for this user
let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data
ctx.insert("votes", &votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
} else {
// Redirect to login if not logged in
Ok(HttpResponse::Found().append_header(("Location", "/login")).finish())
}
// Add user to context (will always be available with our mock user)
let user = Self::get_user_from_session(&session).unwrap();
ctx.insert("user", &user);
// Get mock votes for this user
let votes = Self::get_mock_votes_for_user(1); // Assuming user ID 1 for mock data
ctx.insert("votes", &votes);
render_template(&tmpl, "governance/my_votes.html", &ctx)
}
/// Generate mock recent activity data for the dashboard
fn get_mock_recent_activity() -> Vec<serde_json::Value> {
vec![
serde_json::json!({
"type": "vote",
"user": "Sarah Johnson",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(2)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "comment",
"user": "Michael Chen",
"proposal_id": "prop-003",
"proposal_title": "Weekly Community Calls",
"action": "commented",
"comment": "I think this would greatly improve communication.",
"timestamp": (Utc::now() - Duration::hours(5)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
serde_json::json!({
"type": "vote",
"user": "Robert Callingham",
"proposal_id": "prop-005",
"proposal_title": "Security Audit Implementation",
"action": "voted Yes",
"timestamp": (Utc::now() - Duration::hours(8)).to_rfc3339(),
"icon": "bi-check-circle-fill text-success"
}),
serde_json::json!({
"type": "proposal",
"user": "Emma Rodriguez",
"proposal_id": "prop-004",
"proposal_title": "Sustainability Roadmap",
"action": "created proposal",
"timestamp": (Utc::now() - Duration::hours(12)).to_rfc3339(),
"icon": "bi-file-earmark-text-fill text-info"
}),
serde_json::json!({
"type": "vote",
"user": "David Kim",
"proposal_id": "prop-002",
"proposal_title": "Governance Framework Update",
"action": "voted No",
"timestamp": (Utc::now() - Duration::hours(16)).to_rfc3339(),
"icon": "bi-x-circle-fill text-danger"
}),
serde_json::json!({
"type": "comment",
"user": "Lisa Wang",
"proposal_id": "prop-001",
"proposal_title": "Community Garden Initiative",
"action": "commented",
"comment": "I'd like to volunteer to help coordinate this effort.",
"timestamp": (Utc::now() - Duration::hours(24)).to_rfc3339(),
"icon": "bi-chat-left-text-fill text-primary"
}),
]
}
// Mock data generation methods
@@ -228,9 +309,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 Digital Freezone 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 +321,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: "ZDFZ Sustainable Tourism Framework".to_string(),
description: "A comprehensive framework for sustainable tourism development within the Zanzibar Digital Freezone. 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 +333,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 +345,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: "ZDFZ Regulatory Framework for Digital Financial Services".to_string(),
description: "Establish a comprehensive regulatory framework for digital financial services within the Zanzibar Digital Freezone. 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 +357,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 Artwork Marketplace".to_string(),
description: "Create a dedicated digital arts incubator and Artwork 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 Digital Freezone 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: "ZDFZ Educational Technology Initiative".to_string(),
description: "Establish a comprehensive educational technology program within the Zanzibar Digital Freezone 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 +406,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 +452,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 +462,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 +472,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 +482,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),

View File

@@ -0,0 +1,576 @@
use actix_web::{web, HttpResponse, Result, http};
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use serde::Deserialize;
use uuid::Uuid;
use crate::models::asset::{Asset, AssetType, AssetStatus};
use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
use crate::controllers::asset::AssetController;
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
pub struct ListingForm {
pub title: String,
pub description: String,
pub asset_id: String,
pub price: f64,
pub currency: String,
pub listing_type: String,
pub duration_days: Option<u32>,
pub tags: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct BidForm {
pub amount: f64,
pub currency: String,
}
#[derive(Debug, Deserialize)]
pub struct PurchaseForm {
pub agree_to_terms: bool,
}
pub struct MarketplaceController;
impl MarketplaceController {
// Display the marketplace dashboard
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let listings = Self::get_mock_listings();
let stats = MarketplaceStatistics::new(&listings);
// Get featured listings (up to 4)
let featured_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.featured && l.status == ListingStatus::Active)
.take(4)
.collect();
// Get recent listings (up to 8)
let mut recent_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.status == ListingStatus::Active)
.collect();
// Sort by created_at (newest first)
recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at));
let recent_listings = recent_listings.into_iter().take(8).collect::<Vec<_>>();
// Get recent sales (up to 5)
let mut recent_sales: Vec<&Listing> = listings.iter()
.filter(|l| l.status == ListingStatus::Sold)
.collect();
// Sort by sold_at (newest first)
recent_sales.sort_by(|a, b| {
let a_sold = a.sold_at.unwrap_or(a.created_at);
let b_sold = b.sold_at.unwrap_or(b.created_at);
b_sold.cmp(&a_sold)
});
let recent_sales = recent_sales.into_iter().take(5).collect::<Vec<_>>();
// Add data to context
context.insert("active_page", &"marketplace");
context.insert("stats", &stats);
context.insert("featured_listings", &featured_listings);
context.insert("recent_listings", &recent_listings);
context.insert("recent_sales", &recent_sales);
render_template(&tmpl, "marketplace/index.html", &context)
}
// Display all marketplace listings
pub async fn list_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let listings = Self::get_mock_listings();
// Filter active listings
let active_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.status == ListingStatus::Active)
.collect();
context.insert("active_page", &"marketplace");
context.insert("listings", &active_listings);
context.insert("listing_types", &[
ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(),
ListingType::Exchange.as_str(),
]);
context.insert("asset_types", &[
AssetType::Token.as_str(),
AssetType::Artwork.as_str(),
AssetType::RealEstate.as_str(),
AssetType::IntellectualProperty.as_str(),
AssetType::Commodity.as_str(),
AssetType::Share.as_str(),
AssetType::Bond.as_str(),
AssetType::Other.as_str(),
]);
render_template(&tmpl, "marketplace/listings.html", &context)
}
// Display my listings
pub async fn my_listings(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let listings = Self::get_mock_listings();
// Filter by current user (mock user ID)
let user_id = "user-123";
let my_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.seller_id == user_id)
.collect();
context.insert("active_page", &"marketplace");
context.insert("listings", &my_listings);
render_template(&tmpl, "marketplace/my_listings.html", &context)
}
// Display listing details
pub async fn listing_detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
let listing_id = path.into_inner();
let mut context = Context::new();
let listings = Self::get_mock_listings();
// Find the listing
let listing = listings.iter().find(|l| l.id == listing_id);
if let Some(listing) = listing {
// Get similar listings (same asset type, active)
let similar_listings: Vec<&Listing> = listings.iter()
.filter(|l| l.asset_type == listing.asset_type &&
l.status == ListingStatus::Active &&
l.id != listing.id)
.take(4)
.collect();
// Get highest bid amount and minimum bid for auction listings
let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction {
if let Some(bid) = listing.highest_bid() {
(Some(bid.amount), bid.amount + 1.0)
} else {
(None, listing.price + 1.0)
}
} else {
(None, 0.0)
};
context.insert("active_page", &"marketplace");
context.insert("listing", listing);
context.insert("similar_listings", &similar_listings);
context.insert("highest_bid_amount", &highest_bid_amount);
context.insert("minimum_bid", &minimum_bid);
// Add current user info for bid/purchase forms
let user_id = "user-123";
let user_name = "Alice Hostly";
context.insert("user_id", &user_id);
context.insert("user_name", &user_name);
render_template(&tmpl, "marketplace/listing_detail.html", &context)
} else {
Ok(HttpResponse::NotFound().finish())
}
}
// Display create listing form
pub async fn create_listing_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
// Get user's assets for selection
let assets = AssetController::get_mock_assets();
let user_id = "user-123"; // Mock user ID
let user_assets: Vec<&Asset> = assets.iter()
.filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active)
.collect();
context.insert("active_page", &"marketplace");
context.insert("assets", &user_assets);
context.insert("listing_types", &[
ListingType::FixedPrice.as_str(),
ListingType::Auction.as_str(),
ListingType::Exchange.as_str(),
]);
render_template(&tmpl, "marketplace/create_listing.html", &context)
}
// Create a new listing
pub async fn create_listing(
tmpl: web::Data<Tera>,
form: web::Form<ListingForm>,
) -> Result<HttpResponse> {
let form = form.into_inner();
// Get the asset details
let assets = AssetController::get_mock_assets();
let asset = assets.iter().find(|a| a.id == form.asset_id);
if let Some(asset) = asset {
// Process tags
let tags = match form.tags {
Some(tags_str) => tags_str.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
None => Vec::new(),
};
// Calculate expiration date if provided
let expires_at = form.duration_days.map(|days| {
Utc::now() + Duration::days(days as i64)
});
// Parse listing type
let listing_type = match form.listing_type.as_str() {
"Fixed Price" => ListingType::FixedPrice,
"Auction" => ListingType::Auction,
"Exchange" => ListingType::Exchange,
_ => ListingType::FixedPrice,
};
// Mock user data
let user_id = "user-123";
let user_name = "Alice Hostly";
// Create the listing
let _listing = Listing::new(
form.title,
form.description,
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_id.to_string(),
user_name.to_string(),
form.price,
form.currency,
listing_type,
expires_at,
tags,
asset.image_url.clone(),
);
// In a real application, we would save the listing to a database here
// Redirect to the marketplace
Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, "/marketplace"))
.finish())
} else {
// Asset not found
let mut context = Context::new();
context.insert("active_page", &"marketplace");
context.insert("error", &"Asset not found");
render_template(&tmpl, "marketplace/create_listing.html", &context)
}
}
// Submit a bid on an auction listing
pub async fn submit_bid(
tmpl: web::Data<Tera>,
path: web::Path<String>,
form: web::Form<BidForm>,
) -> Result<HttpResponse> {
let listing_id = path.into_inner();
let form = form.into_inner();
// In a real application, we would:
// 1. Find the listing in the database
// 2. Validate the bid
// 3. Create the bid
// 4. Save it to the database
// For now, we'll just redirect back to the listing
Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
.finish())
}
// Purchase a fixed-price listing
pub async fn purchase_listing(
tmpl: web::Data<Tera>,
path: web::Path<String>,
form: web::Form<PurchaseForm>,
) -> Result<HttpResponse> {
let listing_id = path.into_inner();
let form = form.into_inner();
if !form.agree_to_terms {
// User must agree to terms
return Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id)))
.finish());
}
// In a real application, we would:
// 1. Find the listing in the database
// 2. Validate the purchase
// 3. Process the transaction
// 4. Update the listing status
// 5. Transfer the asset
// For now, we'll just redirect to the marketplace
Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, "/marketplace"))
.finish())
}
// Cancel a listing
pub async fn cancel_listing(
tmpl: web::Data<Tera>,
path: web::Path<String>,
) -> Result<HttpResponse> {
let _listing_id = path.into_inner();
// In a real application, we would:
// 1. Find the listing in the database
// 2. Validate that the current user is the seller
// 3. Update the listing status
// For now, we'll just redirect to my listings
Ok(HttpResponse::SeeOther()
.insert_header((http::header::LOCATION, "/marketplace/my"))
.finish())
}
// Generate mock listings for development
pub fn get_mock_listings() -> Vec<Listing> {
let assets = AssetController::get_mock_assets();
let mut listings = Vec::new();
// Mock user data
let user_ids = vec!["user-123", "user-456", "user-789"];
let user_names = vec!["Alice Hostly", "Ethan Cloudman", "Priya Servera"];
// Create some fixed price listings
for i in 0..6 {
let asset_index = i % assets.len();
let asset = &assets[asset_index];
let user_index = i % user_ids.len();
let price = match asset.asset_type {
AssetType::Token => 50.0 + (i as f64 * 10.0),
AssetType::Artwork => 500.0 + (i as f64 * 100.0),
AssetType::RealEstate => 50000.0 + (i as f64 * 10000.0),
AssetType::IntellectualProperty => 2000.0 + (i as f64 * 500.0),
AssetType::Commodity => 1000.0 + (i as f64 * 200.0),
AssetType::Share => 300.0 + (i as f64 * 50.0),
AssetType::Bond => 1500.0 + (i as f64 * 300.0),
AssetType::Other => 800.0 + (i as f64 * 150.0),
};
let mut listing = Listing::new(
format!("{} for Sale", asset.name),
format!("This is a great opportunity to own {}. {}", asset.name, asset.description),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
Some(Utc::now() + Duration::days(30)),
vec!["digital".to_string(), "asset".to_string()],
asset.image_url.clone(),
);
// Make some listings featured
if i % 5 == 0 {
listing.set_featured(true);
}
listings.push(listing);
}
// Create some auction listings
for i in 0..4 {
let asset_index = (i + 6) % assets.len();
let asset = &assets[asset_index];
let user_index = i % user_ids.len();
let starting_price = match asset.asset_type {
AssetType::Token => 40.0 + (i as f64 * 5.0),
AssetType::Artwork => 400.0 + (i as f64 * 50.0),
AssetType::RealEstate => 40000.0 + (i as f64 * 5000.0),
AssetType::IntellectualProperty => 1500.0 + (i as f64 * 300.0),
AssetType::Commodity => 800.0 + (i as f64 * 100.0),
AssetType::Share => 250.0 + (i as f64 * 40.0),
AssetType::Bond => 1200.0 + (i as f64 * 250.0),
AssetType::Other => 600.0 + (i as f64 * 120.0),
};
let mut listing = Listing::new(
format!("Auction: {}", asset.name),
format!("Bid on this amazing {}. {}", asset.name, asset.description),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
starting_price,
"USD".to_string(),
ListingType::Auction,
Some(Utc::now() + Duration::days(7)),
vec!["auction".to_string(), "bidding".to_string()],
asset.image_url.clone(),
);
// Add some bids to the auctions
let num_bids = 2 + (i % 3);
for j in 0..num_bids {
let bidder_index = (j + 1) % user_ids.len();
if bidder_index != user_index { // Ensure seller isn't bidding
let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64));
let _ = listing.add_bid(
user_ids[bidder_index].to_string(),
user_names[bidder_index].to_string(),
bid_amount,
"USD".to_string(),
);
}
}
// Make some listings featured
if i % 3 == 0 {
listing.set_featured(true);
}
listings.push(listing);
}
// Create some exchange listings
for i in 0..3 {
let asset_index = (i + 10) % assets.len();
let asset = &assets[asset_index];
let user_index = i % user_ids.len();
let value = match asset.asset_type {
AssetType::Token => 60.0 + (i as f64 * 15.0),
AssetType::Artwork => 600.0 + (i as f64 * 150.0),
AssetType::RealEstate => 60000.0 + (i as f64 * 15000.0),
AssetType::IntellectualProperty => 2500.0 + (i as f64 * 600.0),
AssetType::Commodity => 1200.0 + (i as f64 * 300.0),
AssetType::Share => 350.0 + (i as f64 * 70.0),
AssetType::Bond => 1800.0 + (i as f64 * 350.0),
AssetType::Other => 1000.0 + (i as f64 * 200.0),
};
let listing = Listing::new(
format!("Trade: {}", asset.name),
format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
value, // Estimated value for exchange
"USD".to_string(),
ListingType::Exchange,
Some(Utc::now() + Duration::days(60)),
vec!["exchange".to_string(), "trade".to_string()],
asset.image_url.clone(),
);
listings.push(listing);
}
// Create some sold listings
for i in 0..5 {
let asset_index = (i + 13) % assets.len();
let asset = &assets[asset_index];
let seller_index = i % user_ids.len();
let buyer_index = (i + 1) % user_ids.len();
let price = match asset.asset_type {
AssetType::Token => 55.0 + (i as f64 * 12.0),
AssetType::Artwork => 550.0 + (i as f64 * 120.0),
AssetType::RealEstate => 55000.0 + (i as f64 * 12000.0),
AssetType::IntellectualProperty => 2200.0 + (i as f64 * 550.0),
AssetType::Commodity => 1100.0 + (i as f64 * 220.0),
AssetType::Share => 320.0 + (i as f64 * 60.0),
AssetType::Bond => 1650.0 + (i as f64 * 330.0),
AssetType::Other => 900.0 + (i as f64 * 180.0),
};
let sale_price = price * 0.95; // Slight discount on sale
let mut listing = Listing::new(
format!("{} - SOLD", asset.name),
format!("This {} was sold recently.", asset.name),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[seller_index].to_string(),
user_names[seller_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
None,
vec!["sold".to_string()],
asset.image_url.clone(),
);
// Mark as sold
let _ = listing.mark_as_sold(
user_ids[buyer_index].to_string(),
user_names[buyer_index].to_string(),
sale_price,
);
// Set sold date to be sometime in the past
let days_ago = i as i64 + 1;
listing.sold_at = Some(Utc::now() - Duration::days(days_ago));
listings.push(listing);
}
// Create a few cancelled listings
for i in 0..2 {
let asset_index = (i + 18) % assets.len();
let asset = &assets[asset_index];
let user_index = i % user_ids.len();
let price = match asset.asset_type {
AssetType::Token => 45.0 + (i as f64 * 8.0),
AssetType::Artwork => 450.0 + (i as f64 * 80.0),
AssetType::RealEstate => 45000.0 + (i as f64 * 8000.0),
AssetType::IntellectualProperty => 1800.0 + (i as f64 * 400.0),
AssetType::Commodity => 900.0 + (i as f64 * 180.0),
AssetType::Share => 280.0 + (i as f64 * 45.0),
AssetType::Bond => 1350.0 + (i as f64 * 270.0),
AssetType::Other => 750.0 + (i as f64 * 150.0),
};
let mut listing = Listing::new(
format!("{} - Cancelled", asset.name),
format!("This listing for {} was cancelled.", asset.name),
asset.id.clone(),
asset.name.clone(),
asset.asset_type.clone(),
user_ids[user_index].to_string(),
user_names[user_index].to_string(),
price,
"USD".to_string(),
ListingType::FixedPrice,
None,
vec!["cancelled".to_string()],
asset.image_url.clone(),
);
// Cancel the listing
let _ = listing.cancel();
listings.push(listing);
}
listings
}
}

View File

@@ -6,5 +6,9 @@ pub mod calendar;
pub mod governance;
pub mod flow;
pub mod contract;
pub mod asset;
pub mod defi;
pub mod marketplace;
pub mod company;
// Re-export controllers for easier imports

View File

@@ -16,6 +16,7 @@ mod utils;
// Import middleware components
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
use utils::redis_service;
use models::initialize_mock_data;
// Initialize lazy_static for in-memory storage
extern crate lazy_static;
@@ -72,6 +73,10 @@ async fn main() -> io::Result<()> {
log::info!("Redis client initialized successfully");
}
// Initialize mock data for DeFi operations
initialize_mock_data();
log::info!("DeFi mock data initialized successfully");
log::info!("Starting server at http://{}", bind_address);
// Create and configure the HTTP server

View 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 {
Artwork,
Token,
RealEstate,
Commodity,
Share,
Bond,
IntellectualProperty,
Other,
}
impl AssetType {
pub fn as_str(&self) -> &str {
match self {
AssetType::Artwork => "Artwork",
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,
}
}
}

View File

@@ -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",
}
}
@@ -126,6 +136,14 @@ impl ContractRevision {
}
}
/// Table of Contents item for multi-page contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TocItem {
pub title: String,
pub file: String,
pub children: Vec<TocItem>,
}
/// Contract model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
@@ -143,6 +161,9 @@ pub struct Contract {
pub revisions: Vec<ContractRevision>,
pub current_version: u32,
pub organization_id: Option<String>,
// Multi-page markdown support
pub content_dir: Option<String>,
pub toc: Option<Vec<TocItem>>,
}
impl Contract {
@@ -161,8 +182,10 @@ impl Contract {
expiration_date: None,
signers: Vec::new(),
revisions: Vec::new(),
current_version: 0,
current_version: 1,
organization_id,
content_dir: None,
toc: None,
}
}

View File

@@ -0,0 +1,206 @@
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use lazy_static::lazy_static;
use uuid::Uuid;
// DeFi position status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DefiPositionStatus {
Active,
Completed,
Liquidated,
Cancelled
}
impl DefiPositionStatus {
pub fn as_str(&self) -> &str {
match self {
DefiPositionStatus::Active => "Active",
DefiPositionStatus::Completed => "Completed",
DefiPositionStatus::Liquidated => "Liquidated",
DefiPositionStatus::Cancelled => "Cancelled",
}
}
}
// DeFi position type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DefiPositionType {
Providing,
Receiving,
Liquidity,
Staking,
Collateral,
}
impl DefiPositionType {
pub fn as_str(&self) -> &str {
match self {
DefiPositionType::Providing => "Providing",
DefiPositionType::Receiving => "Receiving",
DefiPositionType::Liquidity => "Liquidity",
DefiPositionType::Staking => "Staking",
DefiPositionType::Collateral => "Collateral",
}
}
}
// Base DeFi position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefiPosition {
pub id: String,
pub position_type: DefiPositionType,
pub status: DefiPositionStatus,
pub asset_id: String,
pub asset_name: String,
pub asset_symbol: String,
pub amount: f64,
pub value_usd: f64,
pub expected_return: f64,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub user_id: String,
}
// Providing position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidingPosition {
pub base: DefiPosition,
pub duration_days: i32,
pub profit_share_earned: f64,
pub return_amount: f64,
}
// Receiving position
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceivingPosition {
pub base: DefiPosition,
pub collateral_asset_id: String,
pub collateral_asset_name: String,
pub collateral_asset_symbol: String,
pub collateral_amount: f64,
pub collateral_value_usd: f64,
pub duration_days: i32,
pub profit_share_rate: f64,
pub profit_share_owed: f64,
pub total_to_repay: f64,
pub collateral_ratio: f64,
}
// In-memory database for DeFi positions
pub struct DefiDatabase {
providing_positions: HashMap<String, ProvidingPosition>,
receiving_positions: HashMap<String, ReceivingPosition>,
}
impl DefiDatabase {
pub fn new() -> Self {
Self {
providing_positions: HashMap::new(),
receiving_positions: HashMap::new(),
}
}
// Providing operations
pub fn add_providing_position(&mut self, position: ProvidingPosition) {
self.providing_positions.insert(position.base.id.clone(), position);
}
pub fn get_providing_position(&self, id: &str) -> Option<&ProvidingPosition> {
self.providing_positions.get(id)
}
pub fn get_all_providing_positions(&self) -> Vec<&ProvidingPosition> {
self.providing_positions.values().collect()
}
pub fn get_user_providing_positions(&self, user_id: &str) -> Vec<&ProvidingPosition> {
self.providing_positions
.values()
.filter(|p| p.base.user_id == user_id)
.collect()
}
// Receiving operations
pub fn add_receiving_position(&mut self, position: ReceivingPosition) {
self.receiving_positions.insert(position.base.id.clone(), position);
}
pub fn get_receiving_position(&self, id: &str) -> Option<&ReceivingPosition> {
self.receiving_positions.get(id)
}
pub fn get_all_receiving_positions(&self) -> Vec<&ReceivingPosition> {
self.receiving_positions.values().collect()
}
pub fn get_user_receiving_positions(&self, user_id: &str) -> Vec<&ReceivingPosition> {
self.receiving_positions
.values()
.filter(|p| p.base.user_id == user_id)
.collect()
}
}
// Global instance of the DeFi database
lazy_static! {
pub static ref DEFI_DB: Arc<Mutex<DefiDatabase>> = Arc::new(Mutex::new(DefiDatabase::new()));
}
// Initialize the database with mock data
pub fn initialize_mock_data() {
let mut db = DEFI_DB.lock().unwrap();
// Add mock providing positions
let providing_position = ProvidingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Providing,
status: DefiPositionStatus::Active,
asset_id: "TFT".to_string(),
asset_name: "ThreeFold Token".to_string(),
asset_symbol: "TFT".to_string(),
amount: 1000.0,
value_usd: 500.0,
expected_return: 4.2,
created_at: Utc::now(),
expires_at: Some(Utc::now() + chrono::Duration::days(30)),
user_id: "user123".to_string(),
},
duration_days: 30,
profit_share_earned: 3.5,
return_amount: 1003.5,
};
db.add_providing_position(providing_position);
// Add mock receiving positions
let receiving_position = ReceivingPosition {
base: DefiPosition {
id: Uuid::new_v4().to_string(),
position_type: DefiPositionType::Receiving,
status: DefiPositionStatus::Active,
asset_id: "ZDFZ".to_string(),
asset_name: "Zanzibar Token".to_string(),
asset_symbol: "ZDFZ".to_string(),
amount: 500.0,
value_usd: 250.0,
expected_return: 5.8,
created_at: Utc::now(),
expires_at: Some(Utc::now() + chrono::Duration::days(90)),
user_id: "user123".to_string(),
},
collateral_asset_id: "TFT".to_string(),
collateral_asset_name: "ThreeFold Token".to_string(),
collateral_asset_symbol: "TFT".to_string(),
collateral_amount: 1500.0,
collateral_value_usd: 750.0,
duration_days: 90,
profit_share_rate: 5.8,
profit_share_owed: 3.625,
total_to_repay: 503.625,
collateral_ratio: 300.0,
};
db.add_receiving_position(receiving_position);
}

View File

@@ -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"),
}
}
}

View File

@@ -0,0 +1,295 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::models::asset::{Asset, AssetType};
/// Status of a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListingStatus {
Active,
Sold,
Cancelled,
Expired,
}
impl ListingStatus {
pub fn as_str(&self) -> &str {
match self {
ListingStatus::Active => "Active",
ListingStatus::Sold => "Sold",
ListingStatus::Cancelled => "Cancelled",
ListingStatus::Expired => "Expired",
}
}
}
/// Type of marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ListingType {
FixedPrice,
Auction,
Exchange,
}
impl ListingType {
pub fn as_str(&self) -> &str {
match self {
ListingType::FixedPrice => "Fixed Price",
ListingType::Auction => "Auction",
ListingType::Exchange => "Exchange",
}
}
}
/// Represents a bid on an auction listing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bid {
pub id: String,
pub listing_id: String,
pub bidder_id: String,
pub bidder_name: String,
pub amount: f64,
pub currency: String,
pub created_at: DateTime<Utc>,
pub status: BidStatus,
}
/// Status of a bid
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum BidStatus {
Active,
Accepted,
Rejected,
Cancelled,
}
impl BidStatus {
pub fn as_str(&self) -> &str {
match self {
BidStatus::Active => "Active",
BidStatus::Accepted => "Accepted",
BidStatus::Rejected => "Rejected",
BidStatus::Cancelled => "Cancelled",
}
}
}
/// Represents a marketplace listing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Listing {
pub id: String,
pub title: String,
pub description: String,
pub asset_id: String,
pub asset_name: String,
pub asset_type: AssetType,
pub seller_id: String,
pub seller_name: String,
pub price: f64,
pub currency: String,
pub listing_type: ListingType,
pub status: ListingStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub sold_at: Option<DateTime<Utc>>,
pub buyer_id: Option<String>,
pub buyer_name: Option<String>,
pub sale_price: Option<f64>,
pub bids: Vec<Bid>,
pub views: u32,
pub featured: bool,
pub tags: Vec<String>,
pub image_url: Option<String>,
}
impl Listing {
/// Creates a new listing
pub fn new(
title: String,
description: String,
asset_id: String,
asset_name: String,
asset_type: AssetType,
seller_id: String,
seller_name: String,
price: f64,
currency: String,
listing_type: ListingType,
expires_at: Option<DateTime<Utc>>,
tags: Vec<String>,
image_url: Option<String>,
) -> Self {
let now = Utc::now();
Self {
id: format!("listing-{}", Uuid::new_v4().to_string()),
title,
description,
asset_id,
asset_name,
asset_type,
seller_id,
seller_name,
price,
currency,
listing_type,
status: ListingStatus::Active,
created_at: now,
updated_at: now,
expires_at,
sold_at: None,
buyer_id: None,
buyer_name: None,
sale_price: None,
bids: Vec::new(),
views: 0,
featured: false,
tags,
image_url,
}
}
/// Adds a bid to the listing
pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
if self.listing_type != ListingType::Auction {
return Err("Listing is not an auction".to_string());
}
if currency != self.currency {
return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency));
}
// Check if bid amount is higher than current highest bid or starting price
let highest_bid = self.highest_bid();
let min_bid = match highest_bid {
Some(bid) => bid.amount,
None => self.price,
};
if amount <= min_bid {
return Err(format!("Bid amount must be higher than {}", min_bid));
}
let bid = Bid {
id: format!("bid-{}", Uuid::new_v4().to_string()),
listing_id: self.id.clone(),
bidder_id,
bidder_name,
amount,
currency,
created_at: Utc::now(),
status: BidStatus::Active,
};
self.bids.push(bid);
self.updated_at = Utc::now();
Ok(())
}
/// Gets the highest bid on the listing
pub fn highest_bid(&self) -> Option<&Bid> {
self.bids.iter()
.filter(|b| b.status == BidStatus::Active)
.max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
}
/// Marks the listing as sold
pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
self.status = ListingStatus::Sold;
self.sold_at = Some(Utc::now());
self.buyer_id = Some(buyer_id);
self.buyer_name = Some(buyer_name);
self.sale_price = Some(sale_price);
self.updated_at = Utc::now();
Ok(())
}
/// Cancels the listing
pub fn cancel(&mut self) -> Result<(), String> {
if self.status != ListingStatus::Active {
return Err("Listing is not active".to_string());
}
self.status = ListingStatus::Cancelled;
self.updated_at = Utc::now();
Ok(())
}
/// Increments the view count
pub fn increment_views(&mut self) {
self.views += 1;
}
/// Sets the listing as featured
pub fn set_featured(&mut self, featured: bool) {
self.featured = featured;
self.updated_at = Utc::now();
}
}
/// Statistics for marketplace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketplaceStatistics {
pub total_listings: usize,
pub active_listings: usize,
pub sold_listings: usize,
pub total_value: f64,
pub total_sales: f64,
pub listings_by_type: std::collections::HashMap<String, usize>,
pub sales_by_asset_type: std::collections::HashMap<String, f64>,
}
impl MarketplaceStatistics {
pub fn new(listings: &[Listing]) -> Self {
let mut total_value = 0.0;
let mut total_sales = 0.0;
let mut listings_by_type = std::collections::HashMap::new();
let mut sales_by_asset_type = std::collections::HashMap::new();
let active_listings = listings.iter()
.filter(|l| l.status == ListingStatus::Active)
.count();
let sold_listings = listings.iter()
.filter(|l| l.status == ListingStatus::Sold)
.count();
for listing in listings {
if listing.status == ListingStatus::Active {
total_value += listing.price;
}
if listing.status == ListingStatus::Sold {
if let Some(sale_price) = listing.sale_price {
total_sales += sale_price;
let asset_type = listing.asset_type.as_str().to_string();
*sales_by_asset_type.entry(asset_type).or_insert(0.0) += sale_price;
}
}
let listing_type = listing.listing_type.as_str().to_string();
*listings_by_type.entry(listing_type).or_insert(0) += 1;
}
Self {
total_listings: listings.len(),
active_listings,
sold_listings,
total_value,
total_sales,
listings_by_type,
sales_by_asset_type,
}
}
}

View File

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

View File

@@ -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");
}
}

View File

@@ -7,6 +7,10 @@ use crate::controllers::calendar::CalendarController;
use crate::controllers::governance::GovernanceController;
use crate::controllers::flow::FlowController;
use crate::controllers::contract::ContractController;
use crate::controllers::asset::AssetController;
use crate::controllers::marketplace::MarketplaceController;
use crate::controllers::defi::DefiController;
use crate::controllers::company::CompanyController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
@@ -61,8 +65,8 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
.route("/governance/create-proposal", web::get().to(GovernanceController::create_proposal_form))
.route("/governance/create-proposal", web::post().to(GovernanceController::submit_proposal))
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
// Flow routes
@@ -89,6 +93,55 @@ 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))
)
// Marketplace routes
.service(
web::scope("/marketplace")
.route("", web::get().to(MarketplaceController::index))
.route("/listings", web::get().to(MarketplaceController::list_listings))
.route("/my", web::get().to(MarketplaceController::my_listings))
.route("/create", web::get().to(MarketplaceController::create_listing_form))
.route("/create", web::post().to(MarketplaceController::create_listing))
.route("/{id}", web::get().to(MarketplaceController::listing_detail))
.route("/{id}/bid", web::post().to(MarketplaceController::submit_bid))
.route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing))
.route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing))
)
// DeFi routes
.service(
web::scope("/defi")
.route("", web::get().to(DefiController::index))
.route("/providing", web::post().to(DefiController::create_providing))
.route("/receiving", web::post().to(DefiController::create_receiving))
.route("/liquidity", web::post().to(DefiController::add_liquidity))
.route("/staking", web::post().to(DefiController::create_staking))
.route("/swap", web::post().to(DefiController::swap_tokens))
.route("/collateral", web::post().to(DefiController::create_collateral))
)
// Company routes
.service(
web::scope("/company")
.route("", web::get().to(CompanyController::index))
.route("/register", web::post().to(CompanyController::register))
.route("/view/{id}", web::get().to(CompanyController::view_company))
.route("/switch/{id}", web::get().to(CompanyController::switch_entity))
)
);
// Keep the /protected scope for any future routes that should be under that path

View File

@@ -0,0 +1,173 @@
// Company data (would be loaded from backend in production)
var companyData = {
'company1': {
name: 'Zanzibar Digital Solutions',
type: 'Startup FZC',
status: 'Active',
registrationDate: '2025-04-01',
purpose: 'Digital solutions and blockchain development',
plan: 'Startup FZC - $50/month',
nextBilling: '2025-06-01',
paymentMethod: 'Credit Card (****4582)',
shareholders: [
{ name: 'John Smith', percentage: '60%' },
{ name: 'Sarah Johnson', percentage: '40%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' }
]
},
'company2': {
name: 'Blockchain Innovations Ltd',
type: 'Growth FZC',
status: 'Active',
registrationDate: '2025-03-15',
purpose: 'Blockchain technology research and development',
plan: 'Growth FZC - $100/month',
nextBilling: '2025-06-15',
paymentMethod: 'Bank Transfer',
shareholders: [
{ name: 'Michael Chen', percentage: '35%' },
{ name: 'Aisha Patel', percentage: '35%' },
{ name: 'David Okonkwo', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' },
{ name: 'Physical Asset Holding', status: 'Signed' }
]
},
'company3': {
name: 'Sustainable Energy Cooperative',
type: 'Cooperative FZC',
status: 'Pending',
registrationDate: '2025-05-01',
purpose: 'Renewable energy production and distribution',
plan: 'Cooperative FZC - $200/month',
nextBilling: 'Pending Activation',
paymentMethod: 'Pending',
shareholders: [
{ name: 'Community Energy Group', percentage: '40%' },
{ name: 'Green Future Initiative', percentage: '30%' },
{ name: 'Sustainable Living Collective', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Cooperative Governance', status: 'Pending' }
]
}
};
// Current company ID for modal
var currentCompanyId = null;
// View company details function
function viewCompanyDetails(companyId) {
// Store current company ID
currentCompanyId = companyId;
// Get company data
const company = companyData[companyId];
if (!company) return;
// Update modal title
document.getElementById('companyDetailsModalLabel').innerHTML =
`<i class="bi bi-building me-2"></i>${company.name} Details`;
// Update general information
document.getElementById('modal-company-name').textContent = company.name;
document.getElementById('modal-company-type').textContent = company.type;
document.getElementById('modal-registration-date').textContent = company.registrationDate;
// Update status with appropriate badge
const statusBadge = company.status === 'Active' ?
`<span class="badge bg-success">${company.status}</span>` :
`<span class="badge bg-warning text-dark">${company.status}</span>`;
document.getElementById('modal-status').innerHTML = statusBadge;
document.getElementById('modal-purpose').textContent = company.purpose;
// Update billing information
document.getElementById('modal-plan').textContent = company.plan;
document.getElementById('modal-next-billing').textContent = company.nextBilling;
document.getElementById('modal-payment-method').textContent = company.paymentMethod;
// Update shareholders table
const shareholdersTable = document.getElementById('modal-shareholders');
shareholdersTable.innerHTML = '';
company.shareholders.forEach(shareholder => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${shareholder.name}</td>
<td>${shareholder.percentage}</td>
`;
shareholdersTable.appendChild(row);
});
// Update contracts table
const contractsTable = document.getElementById('modal-contracts');
contractsTable.innerHTML = '';
company.contracts.forEach(contract => {
const row = document.createElement('tr');
const statusBadge = contract.status === 'Signed' ?
`<span class="badge bg-success">${contract.status}</span>` :
`<span class="badge bg-warning text-dark">${contract.status}</span>`;
row.innerHTML = `
<td>${contract.name}</td>
<td>${statusBadge}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="viewContract('${contract.name.toLowerCase().replace(/\s+/g, '-')}')">View</button></td>
`;
contractsTable.appendChild(row);
});
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('companyDetailsModal'));
modal.show();
}
// Switch to entity function
function switchToEntity(companyId) {
const company = companyData[companyId];
if (!company) return;
// In a real application, this would redirect to the entity context
// For now, we'll just show an alert
alert(`Switching to ${company.name} entity context. All UI will now reflect this entity's governance, billing, and other features.`);
// This would typically involve:
// 1. Setting a session/cookie for the current entity
// 2. Redirecting to the dashboard with that entity context
// window.location.href = `/dashboard?entity=${companyId}`;
}
// Switch to entity from modal
function switchToEntityFromModal() {
if (currentCompanyId) {
switchToEntity(currentCompanyId);
// Close the modal
const modal = bootstrap.Modal.getInstance(document.getElementById('companyDetailsModal'));
modal.hide();
}
}
// View contract function
function viewContract(contractId) {
// In a real application, this would open the contract document
// For now, we'll just show an alert
alert(`Viewing contract: ${contractId.replace(/-/g, ' ')}`);
// This would typically involve:
// 1. Fetching the contract document from the server
// 2. Opening it in a viewer or new tab
// window.open(`/contracts/view/${contractId}`, '_blank');
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Company management script loaded');
});

View File

@@ -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))
}
}
}

View File

@@ -1,13 +1,13 @@
{% extends "base.html" %}
{% block title %}About - Zanzibar Autonomous Zone{% endblock %}
{% block title %}About - Zanzibar Digital Freezone{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title">About Zanzibar Autonomous Zone</h1>
<h1 class="card-title">About Zanzibar Digital Freezone</h1>
<p class="card-text">Convenience, Safety and Privacy</p>
<h2 class="mt-4">Technology Stack</h2>

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

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

View File

@@ -0,0 +1,197 @@
{% 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="bi bi-arrow-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="bi bi-arrow-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="bi bi-arrow-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="bi bi-arrow-right"></i></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>Valuation</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for asset in recent_assets %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if asset.asset_type == "Token" %}
<i class="bi bi-coin me-2 text-warning"></i>
{% elif asset.asset_type == "Artwork" %}
<i class="bi bi-image me-2 text-primary"></i>
{% elif asset.asset_type == "Real Estate" %}
<i class="bi bi-building me-2 text-success"></i>
{% elif asset.asset_type == "Intellectual Property" %}
<i class="bi bi-file-earmark-text me-2 text-info"></i>
{% elif asset.asset_type == "Share" %}
<i class="bi bi-graph-up me-2 text-danger"></i>
{% elif asset.asset_type == "Bond" %}
<i class="bi bi-cash-stack me-2 text-secondary"></i>
{% elif asset.asset_type == "Commodity" %}
<i class="bi bi-box me-2 text-dark"></i>
{% else %}
<i class="bi bi-question-circle me-2"></i>
{% endif %}
{{ asset.name }}
</div>
</td>
<td>{{ asset.asset_type }}</td>
<td>
<span class="badge {% if asset.status == 'Active' %}bg-success{% elif asset.status == 'Locked' %}bg-warning{% elif asset.status == 'For Sale' %}bg-info{% elif asset.status == 'Transferred' %}bg-secondary{% else %}bg-dark{% endif %}">
{{ asset.status }}
</span>
</td>
<td>
{% if asset.current_valuation %}
${{ asset.current_valuation }}
{% else %}
<span class="text-muted">Not valued</span>
{% endif %}
</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>
<!-- 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>
<div class="d-flex align-items-center">
{% if asset_type.type == "Token" %}
<i class="bi bi-coin me-2 text-warning"></i>
{% elif asset_type.type == "Artwork" %}
<i class="bi bi-image me-2 text-primary"></i>
{% elif asset_type.type == "Real Estate" %}
<i class="bi bi-building me-2 text-success"></i>
{% elif asset_type.type == "Intellectual Property" %}
<i class="bi bi-file-earmark-text me-2 text-info"></i>
{% elif asset_type.type == "Share" %}
<i class="bi bi-graph-up me-2 text-danger"></i>
{% elif asset_type.type == "Bond" %}
<i class="bi bi-cash-stack me-2 text-secondary"></i>
{% elif asset_type.type == "Commodity" %}
<i class="bi bi-box me-2 text-dark"></i>
{% else %}
<i class="bi bi-question-circle me-2"></i>
{% endif %}
{{ asset_type.type }}
</div>
</td>
<td>{{ asset_type.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/defi.js"></script>

View 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="Artwork">Artwork</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 %}

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

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Zanzibar Autonomous Zone{% endblock %}</title>
<title>{% block title %}Zanzibar Digital Freezone{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/unpoly@3.7.2/unpoly.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
@@ -64,7 +64,21 @@
<button class="navbar-toggler d-md-none me-2" type="button" id="sidebarToggle" aria-label="Toggle navigation">
<i class="bi bi-list text-white"></i>
</button>
<h5 class="mb-0">Zanzibar Autonomous Zone</h5>
<h5 class="mb-0">Zanzibar Digital Freezone {% if entity_name %}| <span class="text-info">{{ entity_name }}</span>{% endif %}</h5>
</div>
<div class="d-none d-md-flex">
<ul class="navbar-nav flex-row">
<li class="nav-item mx-3">
<a class="nav-link text-white {% if active_page == 'about' %}active{% endif %}" target="_blank" href="https://info.ourworld.tf/zdfz">
About
</a>
</li>
<li class="nav-item mx-3">
<a class="nav-link text-white {% if active_page == 'contact' %}active{% endif %}" href="/contact">
Contact
</a>
</li>
</ul>
</div>
<div>
<ul class="navbar-nav flex-row">
@@ -76,6 +90,8 @@
<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="/marketplace/my">My Listings</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>
@@ -97,7 +113,7 @@
</div>
</header>
<div>
<div class="d-flex flex-column min-vh-100">
<!-- Sidebar -->
<div class="sidebar bg-light shadow-sm border-end d-flex" id="sidebar">
<div class="py-2">
@@ -107,11 +123,13 @@
<i class="bi bi-house-door me-2"></i> Home
</a>
</li>
<!-- Support Tickets link hidden
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'tickets' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/tickets">
<i class="bi bi-ticket-perforated me-2"></i> Support Tickets
</a>
</li>
-->
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'governance' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/governance">
<i class="bi bi-people me-2"></i> Governance
@@ -127,32 +145,44 @@
<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 == 'defi' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/defi">
<i class="bi bi-bank me-2"></i> DeFi Platform
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'company' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/company">
<i class="bi bi-building me-2"></i> Companies
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'marketplace' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/marketplace">
<i class="bi bi-shop me-2"></i> Marketplace
</a>
</li>
<!-- Markdown Editor link hidden
<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
</a>
</li>
-->
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'calendar' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/calendar">
<i class="bi bi-calendar3 me-2"></i> Calendar
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'about' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/about">
<i class="bi bi-info-circle me-2"></i> About
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'contact' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/contact">
<i class="bi bi-envelope me-2"></i> Contact
</a>
</li>
</ul>
</div>
</div>
<!-- Main Content -->
<div class="main-content" >
<div class="main-content flex-grow-1">
<!-- Page Content -->
<main class="py-3 w-100 d-block">
<div class="container-fluid">
@@ -160,20 +190,54 @@
</div>
</main>
</div>
<!-- Footer - Full Width -->
<footer class="footer bg-dark text-white">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-md-4 text-center text-md-start mb-2 mb-md-0">
<small>Convenience, Safety and Privacy</small>
</div>
<div class="col-md-4 text-center mb-2 mb-md-0">
<a class="text-white text-decoration-none mx-2" target="_blank" href="https://info.ourworld.tf/zdfz">About</a>
<span class="text-white">|</span>
<a class="text-white text-decoration-none mx-2" href="/contact">Contact</a>
</div>
<div class="col-md-4 text-center text-md-end">
<small>&copy; 2024 Zanzibar Digital Freezone</small>
</div>
</div>
</div>
</footer>
</div>
<!-- Footer - Full Width -->
<footer class="footer bg-dark text-white mt-auto">
<div class="container-fluid d-flex justify-content-between align-items-center">
<div>
<span>Convenience, Safety and Privacy</span>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
{% if success %}
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-success text-white">
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div>
<span>&copy; {{ now(year=true) }} Zanzibar Autonomous Zone. All rights reserved.</span>
<div class="toast-body">
{{ success }}
</div>
</div>
</footer>
{% endif %}
{% if error %}
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header bg-danger text-white">
<strong class="me-auto">Error</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ error }}
</div>
</div>
{% endif %}
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/unpoly@3.7.2/unpoly.min.js"></script>
<script src="https://unpkg.com/unpoly@3.7.2/unpoly-bootstrap5.min.js"></script>
@@ -182,6 +246,17 @@
document.getElementById('sidebarToggle').addEventListener('click', function() {
document.getElementById('sidebar').classList.toggle('show');
});
// Auto-hide toasts after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
const toasts = document.querySelectorAll('.toast.show');
toasts.forEach(toast => {
setTimeout(() => {
const bsToast = new bootstrap.Toast(toast);
bsToast.hide();
}, 5000);
});
});
</script>
{% block extra_js %}{% endblock %}
</body>

View File

@@ -0,0 +1,111 @@
{% extends "base.html" %}
{% block title %}Company Management{% endblock %}
{% block head %}
{{ super() }}
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
</style>
{% endblock %}
{% block content %}
<!-- Toast notification for success messages -->
{% if success_message %}
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-header bg-success text-white">
<i class="bi bi-check-circle me-2"></i>
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ success_message }}
</div>
</div>
</div>
{% endif %}
<div class="container-fluid py-4">
<h2 class="mb-4">Company & Legal Entity Management (Freezone)</h2>
<!-- Company Management Tabs -->
<div class="mb-4">
<div class="card-body">
<ul class="nav nav-tabs" id="companyTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="true">
<i class="bi bi-building me-1"></i> Manage Companies
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">
<i class="bi bi-file-earmark-plus me-1"></i> Register New Company
</button>
</li>
</ul>
<div class="tab-content mt-3" id="companyTabsContent">
<div class="tab-pane fade show active" id="manage" role="tabpanel" aria-labelledby="manage-tab">
{% include "company/manage.html" %}
</div>
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
{% include "company/register.html" %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="/static/js/company.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Show toast if success message exists
const urlParams = new URLSearchParams(window.location.search);
const successMessage = urlParams.get('success');
if (successMessage) {
const toastEl = document.querySelector('.toast');
if (toastEl) {
const toastBody = toastEl.querySelector('.toast-body');
toastBody.textContent = decodeURIComponent(successMessage);
const toast = new bootstrap.Toast(toastEl);
toast.show();
// Auto-hide after 5 seconds
setTimeout(function() {
toast.hide();
}, 5000);
}
}
// Handle tab tracking in URL
const tabParam = urlParams.get('tab');
if (tabParam) {
const tabButton = document.querySelector(`button[data-bs-target="#${tabParam}"]`);
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
}
// Update URL when tab changes
const tabButtons = document.querySelectorAll('button[data-bs-toggle="tab"]');
tabButtons.forEach(function(button) {
button.addEventListener('shown.bs.tab', function(event) {
const targetId = event.target.getAttribute('data-bs-target').substring(1);
const url = new URL(window.location);
url.searchParams.set('tab', targetId);
window.history.replaceState({}, '', url);
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,193 @@
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<i class="bi bi-building me-1"></i> Your Companies
</div>
<div class="card-body">
<!-- Company list table -->
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>Date Registered</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Example rows -->
<tr>
<td>Zanzibar Digital Solutions</td>
<td>Startup FZC</td>
<td><span class="badge bg-success">Active</span></td>
<td>2025-04-01</td>
<td>
<div class="btn-group">
<a href="/company/view/company1" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
<a href="/company/switch/company1" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
</div>
</td>
</tr>
<tr>
<td>Blockchain Innovations Ltd</td>
<td>Growth FZC</td>
<td><span class="badge bg-success">Active</span></td>
<td>2025-03-15</td>
<td>
<div class="btn-group">
<a href="/company/view/company2" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
<a href="/company/switch/company2" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
</div>
</td>
</tr>
<tr>
<td>Sustainable Energy Cooperative</td>
<td>Cooperative FZC</td>
<td><span class="badge bg-warning text-dark">Pending</span></td>
<td>2025-05-01</td>
<td>
<div class="btn-group">
<a href="/company/view/company3" class="btn btn-sm btn-outline-primary"><i class="bi bi-eye"></i> View</a>
<a href="/company/switch/company3" class="btn btn-sm btn-primary"><i class="bi bi-box-arrow-in-right"></i> Switch to Entity</a>
</div>
</td>
</tr>
<!-- More rows dynamically rendered here -->
</tbody>
</table>
</div>
</div>
<!-- Company Details Modal -->
<div class="modal fade" id="companyDetailsModal" tabindex="-1" aria-labelledby="companyDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title" id="companyDetailsModalLabel"><i class="bi bi-building me-2"></i>Company Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="companyDetailsContent">
<!-- Company details will be loaded here -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">General Information</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th>Company Name:</th>
<td id="modal-company-name">Zanzibar Digital Solutions</td>
</tr>
<tr>
<th>Type:</th>
<td id="modal-company-type">Startup FZC</td>
</tr>
<tr>
<th>Registration Date:</th>
<td id="modal-registration-date">2025-04-01</td>
</tr>
<tr>
<th>Status:</th>
<td id="modal-status"><span class="badge bg-success">Active</span></td>
</tr>
<tr>
<th>Purpose:</th>
<td id="modal-purpose">Digital solutions and blockchain development</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Billing Information</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th>Plan:</th>
<td id="modal-plan">Startup FZC - $50/month</td>
</tr>
<tr>
<th>Next Billing:</th>
<td id="modal-next-billing">2025-06-01</td>
</tr>
<tr>
<th>Payment Method:</th>
<td id="modal-payment-method">Credit Card (****4582)</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Shareholders</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Name</th>
<th>Percentage</th>
</tr>
</thead>
<tbody id="modal-shareholders">
<tr>
<td>John Smith</td>
<td>60%</td>
</tr>
<tr>
<td>Sarah Johnson</td>
<td>40%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header">Contracts</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr>
<th>Contract</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="modal-contracts">
<tr>
<td>Articles of Incorporation</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
<tr>
<td>Terms & Conditions</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
<tr>
<td>Digital Asset Issuance</td>
<td><span class="badge bg-success">Signed</span></td>
<td><button class="btn btn-sm btn-outline-primary">View</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="switchToEntityFromModal()"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</button>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
<ul class="nav nav-tabs" id="companyTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage" type="button" role="tab" aria-controls="manage" aria-selected="true">
<i class="bi bi-building me-1"></i> Manage Companies
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button" role="tab" aria-controls="register" aria-selected="false">
<i class="bi bi-file-earmark-plus me-1"></i> Register New Company
</button>
</li>
</ul>
<div class="tab-content mt-4" id="companyTabsContent">
<div class="tab-pane fade show active" id="manage" role="tabpanel" aria-labelledby="manage-tab">
{% include "company/manage.html" %}
</div>
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
{% include "company/register.html" %}
</div>
</div>

View File

@@ -0,0 +1,177 @@
{% extends "base.html" %}
{% block title %}{{ company_name }} - Company Details{% endblock %}
{% block head %}
{{ super() }}
<style>
.badge-signed {
background-color: #198754;
color: white;
}
.badge-pending {
background-color: #ffc107;
color: #212529;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-building me-2"></i>{{ company_name }}</h2>
<div>
<a href="/company" class="btn btn-outline-secondary me-2"><i class="bi bi-arrow-left me-1"></i>Back to Companies</a>
<a href="/company/switch/{{ company_id }}" class="btn btn-primary"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>General Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th style="width: 30%">Company Name:</th>
<td>{{ company_name }}</td>
</tr>
<tr>
<th>Type:</th>
<td>{{ company_type }}</td>
</tr>
<tr>
<th>Registration Date:</th>
<td>{{ registration_date }}</td>
</tr>
<tr>
<th>Status:</th>
<td>
{% if status == "Active" %}
<span class="badge bg-success">{{ status }}</span>
{% else %}
<span class="badge bg-warning text-dark">{{ status }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>Purpose:</th>
<td>{{ purpose }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-credit-card me-2"></i>Billing Information</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th style="width: 30%">Plan:</th>
<td>{{ plan }}</td>
</tr>
<tr>
<th>Next Billing:</th>
<td>{{ next_billing }}</td>
</tr>
<tr>
<th>Payment Method:</th>
<td>{{ payment_method }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-people me-2"></i>Shareholders</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{% for shareholder in shareholders %}
<tr>
<td>{{ shareholder.0 }}</td>
<td>{{ shareholder.1 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-file-earmark-text me-2"></i>Contracts</h5>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>Contract</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>{{ contract.0 }}</td>
<td>
{% if contract.1 == "Signed" %}
<span class="badge bg-success">{{ contract.1 }}</span>
{% else %}
<span class="badge bg-warning text-dark">{{ contract.1 }}</span>
{% endif %}
</td>
<td>
<a href="/contracts/view/{{ contract.0 | lower | replace(from=' ', to='-') }}" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-gear me-2"></i>Actions</h5>
</div>
<div class="card-body">
<div class="d-flex gap-2">
<a href="/company/edit/{{ company_id }}" class="btn btn-outline-primary"><i class="bi bi-pencil me-1"></i>Edit Company</a>
<a href="/company/documents/{{ company_id }}" class="btn btn-outline-secondary"><i class="bi bi-file-earmark me-1"></i>Manage Documents</a>
<a href="/company/switch/{{ company_id }}" class="btn btn-primary"><i class="bi bi-box-arrow-in-right me-1"></i>Switch to Entity</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Company view page loaded');
});
</script>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Contact - Zanzibar Autonomous Zone{% endblock %}
{% block title %}Contact - Zanzibar Digital Freezone{% endblock %}
{% block content %}
<div class="row">
@@ -37,15 +37,15 @@
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Email</h5>
<p class="card-text">info@example.com</p>
<p class="card-text">info@ourworld.tf</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">GitHub</h5>
<p class="card-text">github.com/example/zanzibar-autonomous-zone</p>
<h5 class="card-title">Website</h5>
<p class="card-text">https://info.ourworld.tf/zdfz</p>
</div>
</div>
</div>

View File

@@ -1,3 +1,5 @@
{% import "contracts/macros/contract_macros.html" as contract_macros %}
{% extends "base.html" %}
{% block title %}Contract Details{% endblock %}
@@ -24,302 +26,360 @@
</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>
<li class="nav-item" role="presentation">
<button class="nav-link" id="signatures-tab" data-bs-toggle="tab" data-bs-target="#signatures" type="button" role="tab" aria-controls="signatures" aria-selected="false">
<i class="bi bi-pencil-square me-1"></i> Signatures
</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_section_content_error is defined %}
<div class="alert alert-danger">{{ contract_section_content_error }}</div>
{% endif %}
{% if contract_section_content is defined %}
<div class="row">
<div class="col-md-3">
<div class="list-group mb-3">
{% set section_param = section | default(value=toc[0].file) %}
{{ contract_macros::render_toc(items=toc, section_param=section_param) }}
</div>
</div>
<div class="col-md-9">
<div class="bg-white p-4 border rounded">
{{ contract_section_content | safe }}
</div>
</div>
</div>
{% elif 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="alert alert-warning text-center py-5">
<p>
{% if contract_section_content_error is defined %}
{{ contract_section_content_error }}
{% else %}
No content or markdown sections could be loaded for this contract. Please check the contract's content directory and Table of Contents configuration.
{% endif %}
</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
</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 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 %}
<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>
<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 %}
</tbody>
</table>
</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>
{% 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 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>
<!-- Signatures Tab -->
<div class="tab-pane fade" id="signatures" role="tabpanel" aria-labelledby="signatures-tab">
<div class="row">
<div class="col-md-12">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Signatures</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Email</th>
<th scope="col">Status</th>
<th scope="col">Signed At</th>
<th scope="col">Comments</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for signer in contract.signers %}
<tr class="{% if signer.status == 'Signed' %}table-success{% elif signer.status == 'Rejected' %}table-danger{% elif signer.status == 'Pending' %}table-warning{% endif %}">
<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.status == 'Signed' or signer.status == 'Rejected' %}
{{ signer.signed_at }}
{% else %}
<span class="text-muted">--</span>
{% endif %}
</td>
<td>
{% if signer.comments %}
<span class="small">{{ signer.comments }}</span>
{% else %}
<span class="text-muted">--</span>
{% endif %}
</td>
<td>
{% if signer.status == 'Signed' %}
<a href="/contracts/{{ contract.id }}/signed/{{ signer.id }}" class="btn btn-outline-primary btn-sm" target="_blank">
<i class="bi bi-eye"></i> View Signed Document
</a>
{% elif signer.status == 'Rejected' %}
<button class="btn btn-outline-secondary btn-sm" disabled title="Rejected">
<i class="bi bi-x-circle"></i> Rejected
</button>
<button class="btn btn-outline-warning btn-sm">
<i class="bi bi-bell"></i> Remind to Sign
</button>
{% else %}
{% if current_user is defined and not user_has_signed and signer.email == current_user.email %}
<button class="btn btn-primary btn-sm btn-sign" data-signer-id="{{ signer.id }}">
<i class="bi bi-pen"></i> Sign Here
</button>
{% endif %}
<button class="btn btn-outline-warning btn-sm">
<i class="bi bi-bell"></i> Remind to Sign
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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">
<!-- 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 Digital Freezone
</p>
{% else %}
<p class="text-muted">No organization specified</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- 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 +388,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 %}

View File

@@ -0,0 +1,10 @@
{% macro render_toc(items, section_param) %}
{% for item in items %}
<a href="?section={{ item.file }}" class="list-group-item list-group-item-action{% if section_param == item.file %} active{% endif %}">{{ item.title }}</a>
{% if item.children and item.children | length > 0 %}
<div class="ms-3">
{{ self::render_toc(items=item.children, section_param=section_param) }}
</div>
{% endif %}
{% endfor %}
{% endmacro %}

View File

@@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.token-icon {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #6c757d;
color: white;
font-weight: bold;
font-size: 12px;
}
</style>
{% endblock %}
{% block title %}DeFi Platform{% endblock %}
{% block content %}
<!-- Toast notification for success messages -->
{% if success_message %}
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="5000">
<div class="toast-header bg-success text-white">
<i class="bi bi-check-circle me-2"></i>
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ success_message }}
</div>
</div>
</div>
{% endif %}
<!-- DeFi Platform Tabs -->
<div class="mb-4">
<div class="card-body">
<ul class="nav nav-tabs" id="defiTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab" aria-controls="overview" aria-selected="true">
<i class="bi bi-grid me-1"></i> Overview
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="providing-receiving-tab" data-bs-toggle="tab" data-bs-target="#providing-receiving" type="button" role="tab" aria-controls="providing-receiving" aria-selected="false">
<i class="bi bi-cash-coin me-1"></i> Providing & Receiving
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="liquidity-tab" data-bs-toggle="tab" data-bs-target="#liquidity" type="button" role="tab" aria-controls="liquidity" aria-selected="false">
<i class="bi bi-droplet me-1"></i> Liquidity Pools
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="staking-tab" data-bs-toggle="tab" data-bs-target="#staking" type="button" role="tab" aria-controls="staking" aria-selected="false">
<i class="bi bi-lock me-1"></i> Staking
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="swap-tab" data-bs-toggle="tab" data-bs-target="#swap" type="button" role="tab" aria-controls="swap" aria-selected="false">
<i class="bi bi-arrow-left-right me-1"></i> Swap
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="collateral-tab" data-bs-toggle="tab" data-bs-target="#collateral" type="button" role="tab" aria-controls="collateral" aria-selected="false">
<i class="bi bi-shield-lock me-1"></i> Collateral
</button>
</li>
</ul>
<div class="tab-content mt-3" id="defiTabsContent">
{% include "defi/tabs/overview.html" %}
{% include "defi/tabs/providing_receiving.html" %}
{% include "defi/tabs/liquidity.html" %}
{% include "defi/tabs/staking.html" %}
{% include "defi/tabs/swap.html" %}
{% include "defi/tabs/collateral.html" %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="/static/js/defi.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Show toast if success message exists
const urlParams = new URLSearchParams(window.location.search);
const successMessage = urlParams.get('success');
if (successMessage) {
const toastEl = document.getElementById('successToast');
const toastBody = document.querySelector('.toast-body');
toastBody.textContent = decodeURIComponent(successMessage);
const toast = new bootstrap.Toast(toastEl);
toast.show();
// Auto-hide after 5 seconds
setTimeout(function() {
toast.hide();
}, 5000);
}
// Handle tab tracking in URL
const tabParam = urlParams.get('tab');
if (tabParam) {
// Find the tab button that targets this tab
const tabButton = document.querySelector(`button[data-bs-target="#${tabParam}"]`);
if (tabButton) {
const tab = new bootstrap.Tab(tabButton);
tab.show();
}
}
// Update URL when tab changes
const tabButtons = document.querySelectorAll('button[data-bs-toggle="tab"]');
tabButtons.forEach(function(button) {
button.addEventListener('shown.bs.tab', function(event) {
const targetId = event.target.getAttribute('data-bs-target').substring(1);
const url = new URL(window.location);
url.searchParams.set('tab', targetId);
window.history.replaceState({}, '', url);
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,306 @@
<div class="tab-pane fade" id="collateral" role="tabpanel" aria-labelledby="collateral-tab">
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> About Collateralization</h5>
<p>Use your digital assets as collateral to secure loans or generate synthetic assets. Maintain a healthy collateral ratio to avoid liquidation.</p>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mb-4">
<!-- Collateralize Assets -->
<div class="card">
<div class="card-header">
<i class="bi bi-shield-lock me-1"></i> Collateralize Assets
</div>
<div class="card-body">
<form id="collateralForm" action="/defi/collateral" method="post">
<!-- Asset Selection -->
<div class="mb-3">
<label for="collateralAsset" class="form-label">Select Asset to Collateralize</label>
<select class="form-select" id="collateralAsset" name="asset_id" required>
<option value="" selected disabled>Choose an asset</option>
<!-- Tokens -->
<optgroup label="Tokens">
<option value="TFT" data-type="token" data-value="5000" data-amount="10000" data-unit="TFT">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i> ThreeFold Token (TFT) - 10,000 TFT ($5,000)
</option>
<option value="ZDFZ" data-type="token" data-value="2500" data-amount="5000" data-unit="ZDFZ">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i> Zanzibar Token (ZDFZ) - 5,000 ZDFZ ($2,500)
</option>
</optgroup>
<!-- Digital Assets -->
<optgroup label="Digital Assets">
{% for asset in recent_assets %}
{% if asset.status == 'Active' and asset.current_valuation > 0 %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}" data-amount="1" data-unit="{{ asset.asset_type }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</optgroup>
</select>
</div>
<!-- Collateral Amount -->
<div class="mb-3">
<label for="collateralAmount" class="form-label">Amount to Collateralize</label>
<div class="input-group">
<input type="number" class="form-control" id="collateralAmount" name="amount" min="1" step="1" placeholder="Enter amount" required>
<span class="input-group-text" id="collateralUnit">TFT</span>
</div>
<div class="form-text">
Available: <span id="collateralAvailable">10,000 TFT</span> (<span id="collateralAvailableUSD">$5,000</span>)
</div>
</div>
<!-- Collateral Value -->
<div class="mb-3">
<label class="form-label">Collateral Value</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="collateralValue" name="collateral_value" readonly value="0.00">
</div>
</div>
<!-- Loan Purpose -->
<div class="mb-3">
<label for="collateralPurpose" class="form-label">Purpose</label>
<select class="form-select" id="collateralPurpose" name="purpose" required>
<option value="loan">Secure a Loan</option>
<option value="synthetic">Generate Synthetic Assets</option>
<option value="leverage">Leverage Trading</option>
</select>
</div>
<!-- Loan Term (only shown for loans) -->
<div class="mb-3" id="loanTermGroup">
<label for="loanTerm" class="form-label">Loan Term</label>
<select class="form-select" id="loanTerm" name="loan_term">
<option value="30">30 days (3.5% APR)</option>
<option value="90">90 days (5.0% APR)</option>
<option value="180">180 days (6.5% APR)</option>
<option value="365">365 days (8.0% APR)</option>
</select>
</div>
<!-- Loan Amount (only shown for loans) -->
<div class="mb-3" id="loanAmountGroup">
<label for="loanAmount" class="form-label">Loan Amount (Max 75% of Collateral Value)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="loanAmount" name="loan_amount" min="100" step="100" placeholder="Enter loan amount">
<button class="btn btn-outline-secondary" type="button" id="maxLoanButton">MAX</button>
</div>
<div class="form-text">
Maximum Loan: $<span id="maxLoanAmount">0.00</span>
</div>
</div>
<!-- Synthetic Asset (only shown for synthetic assets) -->
<div class="mb-3" id="syntheticAssetGroup" style="display: none;">
<label for="syntheticAsset" class="form-label">Synthetic Asset to Generate</label>
<select class="form-select" id="syntheticAsset" name="synthetic_asset">
<option value="sUSD">Synthetic USD (sUSD)</option>
<option value="sBTC">Synthetic Bitcoin (sBTC)</option>
<option value="sETH">Synthetic Ethereum (sETH)</option>
<option value="sGOLD">Synthetic Gold (sGOLD)</option>
</select>
</div>
<!-- Synthetic Amount (only shown for synthetic assets) -->
<div class="mb-3" id="syntheticAmountGroup" style="display: none;">
<label for="syntheticAmount" class="form-label">Amount to Generate (Max 50% of Collateral Value)</label>
<div class="input-group">
<input type="number" class="form-control" id="syntheticAmount" name="synthetic_amount" min="10" step="10" placeholder="Enter amount">
<span class="input-group-text" id="syntheticUnit">sUSD</span>
<button class="btn btn-outline-secondary" type="button" id="maxSyntheticButton">MAX</button>
</div>
<div class="form-text">
Maximum Amount: <span id="maxSyntheticAmount">0.00</span> <span id="maxSyntheticUnit">sUSD</span>
</div>
</div>
<!-- Collateral Ratio -->
<div class="mb-3">
<label class="form-label">Collateral Ratio</label>
<div class="input-group">
<input type="text" class="form-control" id="collateralRatio" name="collateral_ratio" readonly value="0%">
<span class="input-group-text">
<i class="bi bi-info-circle" data-bs-toggle="tooltip" title="Minimum required ratio: 150% for loans, 200% for synthetic assets"></i>
</span>
</div>
</div>
<!-- Liquidation Price -->
<div class="mb-3">
<label class="form-label">Liquidation Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="liquidationPrice" name="liquidation_price" readonly value="0.00">
<span class="input-group-text">per <span id="liquidationUnit">TFT</span></span>
</div>
<div class="form-text text-danger">
Your collateral will be liquidated if the price falls below this level.
</div>
</div>
<!-- Submit Button -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary" id="collateralizeButton">Collateralize Asset</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<!-- Active Collateral Positions -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-check me-1"></i> Your Active Collateral Positions
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Collateral Value</th>
<th>Borrowed/Generated</th>
<th>Collateral Ratio</th>
<th>Liquidation Price</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
2,000 TFT
</div>
</td>
<td>$1,000</td>
<td>$700 (Loan)</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 70%"></div>
</div>
<span>143%</span>
</div>
</td>
<td>$0.35</td>
<td><span class="badge bg-success">Healthy</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Add</button>
<button class="btn btn-sm btn-outline-warning">Repay</button>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-image me-2 text-primary"></i>
Beach Property Artwork
</div>
</td>
<td>$25,000</td>
<td>10,000 sUSD</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div class="progress-bar bg-warning" role="progressbar" style="width: 40%"></div>
</div>
<span>250%</span>
</div>
</td>
<td>$10,000</td>
<td><span class="badge bg-warning">Warning</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Add</button>
<button class="btn btn-sm btn-outline-warning">Repay</button>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
1,000 ZDFZ
</div>
</td>
<td>$500</td>
<td>0.1 sBTC</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 80%"></div>
</div>
<span>333%</span>
</div>
</td>
<td>$0.15</td>
<td><span class="badge bg-success">Healthy</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Add</button>
<button class="btn btn-sm btn-outline-warning">Repay</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Collateral Health -->
<div class="card">
<div class="card-header">
<i class="bi bi-heart-pulse me-1"></i> Collateral Health
</div>
<div class="card-body">
<div class="mb-4">
<h6>Overall Collateral Health</h6>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 60%;" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">60%</div>
</div>
<div class="small text-muted">
<i class="bi bi-info-circle"></i> Health score represents the overall safety of your collateral positions. Higher is better.
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body p-3">
<h6 class="card-title">Total Collateral Value</h6>
<h3 class="mb-0">$26,500</h3>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body p-3">
<h6 class="card-title">Total Borrowed/Generated</h6>
<h3 class="mb-0">$11,150</h3>
</div>
</div>
</div>
</div>
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle"></i> Your Beach Property Artwork collateral is close to the liquidation threshold. Consider adding more collateral or repaying part of your synthetic assets.
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,281 @@
<div class="tab-pane fade" id="providing" role="tabpanel" aria-labelledby="providing-tab">
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<i class="bi bi-box-arrow-right me-1"></i> Provide Your Assets
</div>
<div class="card-body">
<p class="card-text">Earn profit share by providing your digital assets to the ZDFZ DeFi platform.</p>
<form action="/defi/providing" method="post">
<div class="mb-3">
<label for="asset" class="form-label">Select Asset</label>
<select class="form-select" id="asset" name="asset_id" required>
<option value="" selected disabled>Choose an asset to provide</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="amount" class="form-label">Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="amount" name="amount" min="0.01" step="0.01" required>
<span class="input-group-text" id="assetSymbol">TFT</span>
</div>
</div>
<div class="mb-3">
<label for="duration" class="form-label">Duration</label>
<select class="form-select" id="duration" name="duration" required>
<option value="7">7 days (2.5% Expected Return %)</option>
<option value="30" selected>30 days (4.2% Expected Return %)</option>
<option value="90">90 days (6.8% Expected Return %)</option>
<option value="180">180 days (8.5% Expected Return %)</option>
<option value="365">365 days (12.0% Expected Return %)</option>
</select>
</div>
<div class="alert alert-success">
<div class="d-flex justify-content-between">
<span>Estimated Profit Share:</span>
<strong id="profitShareEstimate">0.00 TFT</strong>
</div>
<div class="d-flex justify-content-between">
<span>Expected Return:</span>
<strong id="returnAmount">0.00 TFT</strong>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Provide Asset</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-success text-white">
<i class="bi bi-box-arrow-in-left me-1"></i> Receive Against Assets
</div>
<div class="card-body">
<p class="card-text">Receive digital assets by contributing your existing assets as security.</p>
<form action="/defi/receiving" method="post">
<div class="mb-3">
<label for="collateralAsset" class="form-label">Collateral Asset</label>
<select class="form-select" id="collateralAsset" name="collateral_asset_id" required>
<option value="" selected disabled>Choose an asset as collateral</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="receivingAsset" class="form-label">Asset to Receive</label>
<select class="form-select" id="receivingAsset" name="asset_id" required>
<option value="TFT" selected>ThreeFold Token (TFT)</option>
<option value="BTC">Bitcoin (BTC)</option>
<option value="ETH">Ethereum (ETH)</option>
<option value="USDT">Tether (USDT)</option>
<option value="ZDFZ">Zanzibar Token (ZDFZ)</option>
</select>
</div>
<div class="mb-3">
<label for="receivingAmount" class="form-label">Receiving Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="receivingAmount" name="amount" min="0.01" step="0.01" required>
<span class="input-group-text" id="receivingAssetSymbol">TFT</span>
</div>
<div class="form-text">You can receive up to 70% of your collateral value.</div>
</div>
<div class="mb-3">
<label for="receivingTerm" class="form-label">Duration</label>
<select class="form-select" id="receivingTerm" name="duration" required>
<option value="7">7 days (3.5% Expected Return %)</option>
<option value="30" selected>30 days (5.2% Expected Return %)</option>
<option value="90">90 days (7.8% Expected Return %)</option>
<option value="180">180 days (9.5% Expected Return %)</option>
</select>
</div>
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<span>Collateral Ratio:</span>
<strong id="collateralRatio">0%</strong>
</div>
<div class="d-flex justify-content-between">
<span>Obligation Due:</span>
<strong id="obligationDue">0.00 TFT</strong>
</div>
<div class="d-flex justify-content-between">
<span>Total Repayment:</span>
<strong id="totalRepayment">0.00 TFT</strong>
</div>
<div class="progress mt-2">
<div id="collateralRatioBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">Receive Asset</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Active Providing & Receiving Positions -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-check me-1"></i> Your Active Positions
</div>
<div class="card-body">
<ul class="nav nav-pills mb-3" id="positionsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="providing-positions-tab" data-bs-toggle="pill" data-bs-target="#providing-positions" type="button" role="tab" aria-controls="providing-positions" aria-selected="true">Providing</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="receiving-positions-tab" data-bs-toggle="pill" data-bs-target="#receiving-positions" type="button" role="tab" aria-controls="receiving-positions" aria-selected="false">Receiving</button>
</li>
</ul>
<div class="tab-content" id="positionsTabsContent">
<div class="tab-pane fade show active" id="providing-positions" role="tabpanel" aria-labelledby="providing-positions-tab">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Amount</th>
<th>Value</th>
<th>Expected Return %</th>
<th>Start Date</th>
<th>End Date</th>
<th>Profit Share Earned</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if providing_positions and providing_positions|length > 0 %}
{% for position in providing_positions %}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
{{ position.base.asset_name }}
</div>
</td>
<td>{{ position.base.amount }} {{ position.base.asset_symbol }}</td>
<td>${{ position.base.value_usd | round(precision = 2) }}</td>
<td>{{ position.base.expected_return }}%</td>
<td>{{ position.base.created_at | date }}</td>
<td>{{ position.base.expires_at | date }}</td>
<td>{{ position.profit_share_earned | round(precision = 2) }} {{ position.base.asset_symbol }}</td>
<td>
<span class="badge bg-{% if position.base.status == 'Active' %}success{% elif position.base.status == 'Completed' %}info{% elif position.base.status == 'Liquidated' %}danger{% else %}warning{% endif %}">
{{ position.base.status }}
</span>
</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Withdraw</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="9" class="text-center">No active providing positions found</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="receiving-positions" role="tabpanel" aria-labelledby="receiving-positions-tab">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Received Asset</th>
<th>Amount</th>
<th>Collateral</th>
<th>Collateral Ratio</th>
<th>Expected Return %</th>
<th>Start Date</th>
<th>Due Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if receiving_positions and receiving_positions|length > 0 %}
{% for position in receiving_positions %}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
{{ position.base.asset_name }}
</div>
</td>
<td>{{ position.base.amount }} {{ position.base.asset_symbol }}</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
{{ position.collateral_amount }} {{ position.collateral_asset_symbol }}
</div>
</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div class="progress-bar {% if position.collateral_ratio >= 200 %}bg-success{% elif position.collateral_ratio >= 150 %}bg-warning{% else %}bg-danger{% endif %}" role="progressbar" style="width: {% if (position.collateral_ratio / 3) > 100 %}100{% else %}{{ position.collateral_ratio / 3 }}{% endif %}%"></div>
</div>
<span>{{ position.collateral_ratio | round(precision=0) }}%</span>
</div>
</td>
<td>{{ position.base.expected_return }}%</td>
<td>{{ position.base.created_at|date }}</td>
<td>{{ position.base.expires_at|date }}</td>
<td>
<span class="badge bg-{% if position.base.status == 'Active' %}success{% elif position.base.status == 'Completed' %}info{% elif position.base.status == 'Liquidated' %}danger{% else %}warning{% endif %}">
{{ position.base.status }}
</span>
</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Repay</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="9" class="text-center">No active receiving positions found</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,274 @@
<div class="tab-pane fade" id="liquidity" role="tabpanel" aria-labelledby="liquidity-tab">
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> About Liquidity Pools</h5>
<p>Liquidity pools are collections of tokens locked in smart contracts that provide liquidity for decentralized trading. By adding your assets to a liquidity pool, you earn a share of the trading fees generated by the pool.</p>
</div>
</div>
</div>
<!-- Available Liquidity Pools -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-droplet-fill me-1"></i> Available Liquidity Pools
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Pool</th>
<th>Total Liquidity</th>
<th>24h Volume</th>
<th>APY</th>
<th>Your Liquidity</th>
<th>Your Share</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="position-relative me-2">
</div>
TFT-ZDFZ
</div>
</td>
<td>$1,250,000</td>
<td>$45,000</td>
<td>12.5%</td>
<td>$2,500</td>
<td>0.2%</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addLiquidityModal" data-pool="TFT-ZDFZ">Add</button>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#removeLiquidityModal" data-pool="TFT-ZDFZ">Remove</button>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="position-relative me-2">
</div>
TFT-USDT
</div>
</td>
<td>$3,750,000</td>
<td>$125,000</td>
<td>8.2%</td>
<td>$0</td>
<td>0%</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addLiquidityModal" data-pool="TFT-USDT">Add</button>
<button class="btn btn-sm btn-outline-primary" disabled>Remove</button>
</div>
</td>
</tr>
<tr>
<td>
ZDFZ-USDT
</td>
<td>$850,000</td>
<td>$32,000</td>
<td>15.8%</td>
<td>$5,000</td>
<td>0.59%</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addLiquidityModal" data-pool="ZDFZ-USDT">Add</button>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#removeLiquidityModal" data-pool="ZDFZ-USDT">Remove</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Your Liquidity Positions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-wallet2 me-1"></i> Your Liquidity Positions
</div>
<div class="card-body">
<div class="row">
<!-- TFT-ZDFZ Position -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header bg-light">
TFT-ZDFZ
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Your Liquidity:</span>
<strong>$2,500</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Pool Share:</span>
<strong>0.2%</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>TFT:</span>
<strong>500 TFT</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>ZDFZ:</span>
<strong>1,250 ZDFZ</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Earned Fees:</span>
<strong>$45.20</strong>
</div>
<div class="d-flex justify-content-between mb-3">
<span>APY:</span>
<strong class="text-success">12.5%</strong>
</div>
<div class="d-grid gap-2">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addLiquidityModal" data-pool="TFT-ZDFZ">Add Liquidity</button>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#removeLiquidityModal" data-pool="TFT-ZDFZ">Remove Liquidity</button>
<button class="btn btn-sm btn-outline-success">Claim Rewards</button>
</div>
</div>
</div>
</div>
<!-- ZDFZ-USDT Position -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header bg-light">
ZDFZ-USDT
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Your Liquidity:</span>
<strong>$5,000</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Pool Share:</span>
<strong>0.59%</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>ZDFZ:</span>
<strong>2,500 ZDFZ</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>USDT:</span>
<strong>2,500 USDT</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Earned Fees:</span>
<strong>$128.75</strong>
</div>
<div class="d-flex justify-content-between mb-3">
<span>APY:</span>
<strong class="text-success">15.8%</strong>
</div>
<div class="d-grid gap-2">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addLiquidityModal" data-pool="ZDFZ-USDT">Add Liquidity</button>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#removeLiquidityModal" data-pool="ZDFZ-USDT">Remove Liquidity</button>
<button class="btn btn-sm btn-outline-success">Claim Rewards</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create New Liquidity Pool -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-plus-circle me-1"></i> Create New Liquidity Pool
</div>
<div class="card-body">
<form action="/defi/liquidity" method="post">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="firstToken" class="form-label">First Token</label>
<select class="form-select" id="firstToken" name="first_token" required>
<option value="" selected disabled>Select first token</option>
<option value="TFT">ThreeFold Token (TFT)</option>
<option value="ZDFZ">Zanzibar Token (ZDFZ)</option>
<option value="BTC">Bitcoin (BTC)</option>
<option value="ETH">Ethereum (ETH)</option>
<option value="USDT">Tether (USDT)</option>
</select>
</div>
<div class="mb-3">
<label for="firstTokenAmount" class="form-label">Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="firstTokenAmount" name="first_token_amount" min="0.000001" step="0.000001" required>
<span class="input-group-text" id="firstTokenSymbol">TFT</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="secondToken" class="form-label">Second Token</label>
<select class="form-select" id="secondToken" name="second_token" required>
<option value="" selected disabled>Select second token</option>
<option value="TFT">ThreeFold Token (TFT)</option>
<option value="ZDFZ">Zanzibar Token (ZDFZ)</option>
<option value="BTC">Bitcoin (BTC)</option>
<option value="ETH">Ethereum (ETH)</option>
<option value="USDT">Tether (USDT)</option>
</select>
</div>
<div class="mb-3">
<label for="secondTokenAmount" class="form-label">Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="secondTokenAmount" name="second_token_amount" min="0.000001" step="0.000001" required>
<span class="input-group-text" id="secondTokenSymbol">ZDFZ</span>
</div>
</div>
</div>
</div>
<div class="mb-3">
<label for="initialPrice" class="form-label">Initial Price Ratio</label>
<div class="input-group">
<span class="input-group-text">1</span>
<span class="input-group-text" id="firstTokenSymbolRatio">TFT</span>
<span class="input-group-text">=</span>
<input type="number" class="form-control" id="initialPrice" name="initial_price" min="0.000001" step="0.000001" required>
<span class="input-group-text" id="secondTokenSymbolRatio">ZDFZ</span>
</div>
</div>
<div class="mb-3">
<label for="poolFee" class="form-label">Pool Fee</label>
<select class="form-select" id="poolFee" name="pool_fee" required>
<option value="0.1">0.1%</option>
<option value="0.3" selected>0.3%</option>
<option value="0.5">0.5%</option>
<option value="1.0">1.0%</option>
</select>
<div class="form-text">This fee is charged on each trade and distributed to liquidity providers.</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Create Liquidity Pool</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<div class="tab-pane fade show active" id="overview" role="tabpanel" aria-labelledby="overview-tab">
<div class="alert alert-info">
<h4 class="alert-heading"><i class="bi bi-info-circle"></i> Welcome to the ZDFZ DeFi Platform!</h4>
<p>Our decentralized finance platform allows you to maximize the value of your digital assets through various financial services.</p>
<hr>
<p class="mb-0">Use the tabs above to explore lending, borrowing, liquidity pools, staking, swapping, and collateralization features.</p>
</div>
</div>

View File

@@ -0,0 +1,257 @@
{#
This is a compliant version of the previous lending_borrowing.html tab. All terminology is updated to "Providing" and "Receiving".
#}
<div class="tab-pane fade" id="providing-receiving" role="tabpanel" aria-labelledby="providing-receiving-tab">
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<i class="bi bi-box-arrow-right me-1"></i> Provide Your Assets
</div>
<div class="card-body">
<p class="card-text">Earn profit share by providing your digital assets to the ZDFZ DeFi platform.</p>
<form action="/defi/providing" method="post">
<div class="mb-3">
<label for="asset" class="form-label">Select Asset</label>
<select class="form-select" id="asset" name="asset_id" required>
<option value="" selected disabled>Choose an asset to provide</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="amount" class="form-label">Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="amount" name="amount" min="0.01" step="0.01" required>
<span class="input-group-text" id="assetSymbol">TFT</span>
</div>
</div>
<div class="mb-3">
<label for="duration" class="form-label">Duration</label>
<select class="form-select" id="duration" name="duration" required>
<option value="7">7 days (2.5% Expected Return %)</option>
<option value="30" selected>30 days (4.2% Expected Return %)</option>
<option value="90">90 days (6.8% Expected Return %)</option>
<option value="180">180 days (8.5% Expected Return %)</option>
<option value="365">365 days (12.0% Expected Return %)</option>
</select>
</div>
<div class="alert alert-success">
<div class="d-flex justify-content-between">
<span>Estimated Profit Share:</span>
<strong id="profitShareEstimate">0.00 TFT</strong>
</div>
<div class="d-flex justify-content-between">
<span>Expected Return:</span>
<strong id="returnAmount">0.00 TFT</strong>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Provide Asset</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-success text-white">
<i class="bi bi-box-arrow-in-left me-1"></i> Receive Against Assets
</div>
<div class="card-body">
<p class="card-text">Receive digital assets by contributing your existing assets as security.</p>
<form action="/defi/receiving" method="post">
<div class="mb-3">
<label for="collateralAsset" class="form-label">Collateral Asset</label>
<select class="form-select" id="collateralAsset" name="collateral_asset_id" required>
<option value="" selected disabled>Choose a collateral asset</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="collateralAmount" class="form-label">Collateral Amount</label>
<div class="input-group">
<input type="number" class="form-control" id="collateralAmount" name="collateral_amount" min="0.01" step="0.01" required>
<span class="input-group-text" id="collateralAssetSymbol">TFT</span>
</div>
</div>
<div class="mb-3">
<label for="receivingAsset" class="form-label">Asset to Receive</label>
<select class="form-select" id="receivingAsset" name="asset_id" required>
<option value="" selected disabled>Choose an asset to receive</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
<div class="form-text">You can receive up to 70% of your collateral value.</div>
</div>
<div class="mb-3">
<label for="receivingTerm" class="form-label">Duration</label>
<select class="form-select" id="receivingTerm" name="duration" required>
<option value="7">7 days (3.5% Profit Share Rate)</option>
<option value="30" selected>30 days (5.2% Profit Share Rate)</option>
<option value="90">90 days (8.1% Profit Share Rate)</option>
<option value="180">180 days (9.5% Profit Share Rate)</option>
</select>
</div>
<div class="alert alert-warning">
<div class="d-flex justify-content-between">
<span>Collateral Ratio:</span>
<strong id="collateralRatio">0.00%</strong>
</div>
<div class="d-flex justify-content-between">
<span>Profit Share Owed:</span>
<strong id="profitShareOwed">0.00 TFT</strong>
</div>
<div class="d-flex justify-content-between">
<span>Total to Repay:</span>
<strong id="totalToRepay">0.00 TFT</strong>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">Receive Asset</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<i class="bi bi-list-ul me-1"></i> Providing Positions
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Asset</th>
<th>Amount</th>
<th>Expected Return</th>
<th>Start Date</th>
<th>Due Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% if providing_positions and providing_positions|length > 0 %}
{% for position in providing_positions %}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
{{ position.base.asset_name }}
</div>
</td>
<td>{{ position.base.amount }} {{ position.base.asset_symbol }}</td>
<td>{{ position.base.expected_return }}%</td>
<td>{{ position.base.created_at|date }}</td>
<td>{{ position.base.expires_at|date }}</td>
<td>
<span class="badge bg-{% if position.base.status == 'Active' %}success{% elif position.base.status == 'Completed' %}info{% elif position.base.status == 'Liquidated' %}danger{% else %}warning{% endif %}">
{{ position.base.status }}
</span>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" class="text-center">No active providing positions found</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-success text-white">
<i class="bi bi-list-ul me-1"></i> Receiving Positions
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Asset</th>
<th>Amount</th>
<th>Collateral</th>
<th>Collateral Ratio</th>
<th>Profit Share Rate</th>
<th>Start Date</th>
<th>Due Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if receiving_positions and receiving_positions|length > 0 %}
{% for position in receiving_positions %}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
{{ position.base.asset_name }}
</div>
</td>
<td>{{ position.base.amount }} {{ position.base.asset_symbol }}</td>
<td>
{{ position.collateral_amount }} {{ position.collateral_asset_symbol }}
</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div class="progress-bar {% if position.collateral_ratio >= 200 %}bg-success{% elif position.collateral_ratio >= 150 %}bg-warning{% else %}bg-danger{% endif %}" role="progressbar" style="width: {% if (position.collateral_ratio / 3) > 100 %}100{% else %}{{ position.collateral_ratio / 3 }}{% endif %}%"></div>
</div>
<span>{{ position.collateral_ratio | round(precision=0) }}%</span>
</div>
</td>
<td>{{ position.base.expected_return }}%</td>
<td>{{ position.base.created_at|date }}</td>
<td>{{ position.base.expires_at|date }}</td>
<td>
<span class="badge bg-{% if position.base.status == 'Active' %}success{% elif position.base.status == 'Completed' %}info{% elif position.base.status == 'Liquidated' %}danger{% else %}warning{% endif %}">
{{ position.base.status }}
</span>
</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary">Repay</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="9" class="text-center">No active receiving positions found</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,280 @@
<div class="tab-pane fade" id="staking" role="tabpanel" aria-labelledby="staking-tab">
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> About Staking</h5>
<p>Staking allows you to lock your digital assets for a period of time to support network operations and earn rewards. The longer you stake, the higher rewards you can earn.</p>
</div>
</div>
</div>
<!-- Available Staking Options -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-lock-fill me-1"></i> Available Staking Options
</div>
<div class="card-body">
<div class="row">
<!-- TFT Staking -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<div class="d-flex align-items-center">
<h6 class="mb-0">ThreeFold Token (TFT)</h6>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Total Staked:</span>
<strong>5,250,000 TFT</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Your Stake:</span>
<strong>1,000 TFT</strong>
</div>
<div class="d-flex justify-content-between mb-3">
<span>APY:</span>
<strong class="text-success">8.5%</strong>
</div>
<form action="/defi/staking" method="post">
<input type="hidden" name="asset_id" value="TFT">
<div class="mb-3">
<label for="tftStakingPeriod" class="form-label">Staking Period</label>
<select class="form-select" id="tftStakingPeriod" name="staking_period">
<option value="30">30 days (8.5% APY)</option>
<option value="90">90 days (10.2% APY)</option>
<option value="180">180 days (12.5% APY)</option>
<option value="365">365 days (15.0% APY)</option>
</select>
</div>
<div class="mb-3">
<label for="tftStakeAmount" class="form-label">Amount to Stake</label>
<div class="input-group">
<input type="number" class="form-control" id="tftStakeAmount" name="amount" min="100" step="1" placeholder="Min 100 TFT">
<span class="input-group-text">TFT</span>
</div>
</div>
<div class="alert alert-success mb-3">
<div class="d-flex justify-content-between">
<span>Estimated Rewards:</span>
<strong id="tftEstimatedRewards">0 TFT</strong>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary" id="tftStakeButton">Stake TFT</button>
</div>
</form>
</div>
</div>
</div>
<!-- ZDFZ Staking -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header bg-success text-white">
<div class="d-flex align-items-center">
<h6 class="mb-0">Zanzibar Token (ZDFZ)</h6>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span>Total Staked:</span>
<strong>2,750,000 ZDFZ</strong>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Your Stake:</span>
<strong>500 ZDFZ</strong>
</div>
<div class="d-flex justify-content-between mb-3">
<span>APY:</span>
<strong class="text-success">12.0%</strong>
</div>
<form action="/defi/staking" method="post">
<input type="hidden" name="asset_id" value="ZDFZ">
<div class="mb-3">
<label for="zazStakingPeriod" class="form-label">Staking Period</label>
<select class="form-select" id="zazStakingPeriod" name="staking_period">
<option value="30">30 days (12.0% APY)</option>
<option value="90">90 days (14.5% APY)</option>
<option value="180">180 days (16.8% APY)</option>
<option value="365">365 days (20.0% APY)</option>
</select>
</div>
<div class="mb-3">
<label for="zazStakeAmount" class="form-label">Amount to Stake</label>
<div class="input-group">
<input type="number" class="form-control" id="zazStakeAmount" name="amount" min="50" step="1" placeholder="Min 50 ZDFZ">
<span class="input-group-text">ZDFZ</span>
</div>
</div>
<div class="alert alert-success mb-3">
<div class="d-flex justify-content-between">
<span>Estimated Rewards:</span>
<strong id="zazEstimatedRewards">0 ZDFZ</strong>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-success" id="zazStakeButton">Stake ZDFZ</button>
</div>
</form>
</div>
</div>
</div>
<!-- Asset Staking -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header bg-info text-white">
<div class="d-flex align-items-center">
<i class="bi bi-collection me-2"></i>
<h6 class="mb-0">Digital Asset Staking</h6>
</div>
</div>
<div class="card-body">
<p class="card-text">Stake your NFTs and other digital assets to earn passive income.</p>
<form action="/defi/staking" method="post">
<div class="mb-3">
<label for="assetStaking" class="form-label">Select Asset</label>
<select class="form-select" id="assetStaking" name="asset_id">
<option value="" selected disabled>Choose an asset to stake</option>
{% for asset in recent_assets %}
{% if asset.status == 'Active' and asset.current_valuation > 0 %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-value="{{ asset.current_valuation }}" data-amount="1" data-unit="{{ asset.asset_type }}">
{{ asset.name }} ({{ asset.asset_type }}) - ${{ asset.current_valuation }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="assetStakingPeriod" class="form-label">Staking Period</label>
<select class="form-select" id="assetStakingPeriod" name="staking_period">
<option value="30">30 days (3.5% APY)</option>
<option value="90">90 days (5.2% APY)</option>
<option value="180">180 days (7.5% APY)</option>
<option value="365">365 days (10.0% APY)</option>
</select>
</div>
<div class="alert alert-success mb-3">
<div class="d-flex justify-content-between">
<span>Estimated Rewards:</span>
<strong id="assetEstimatedRewards">$0.00</strong>
</div>
<div class="d-flex justify-content-between">
<span>Reward Token:</span>
<strong>ZDFZ</strong>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-info" id="assetStakeButton">Stake Asset</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Your Active Stakes -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-list-check me-1"></i> Your Active Stakes
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Amount</th>
<th>Value</th>
<th>Start Date</th>
<th>End Date</th>
<th>APY</th>
<th>Earned Rewards</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
ThreeFold Token (TFT)
</div>
</td>
<td>1,000 TFT</td>
<td>$500</td>
<td>2025-03-15</td>
<td>2025-06-15</td>
<td>10.2%</td>
<td>22.5 TFT</td>
<td><span class="badge bg-success">Active</span></td>
<td>
<button class="btn btn-sm btn-outline-success">Claim Rewards</button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
Zanzibar Token (ZDFZ)
</div>
</td>
<td>500 ZDFZ</td>
<td>$250</td>
<td>2025-04-01</td>
<td>2025-05-01</td>
<td>12.0%</td>
<td>5.0 ZDFZ</td>
<td><span class="badge bg-success">Active</span></td>
<td>
<button class="btn btn-sm btn-outline-success">Claim Rewards</button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-image me-2 text-primary"></i>
Beach Property Artwork
</div>
</td>
<td>1 Artwork</td>
<td>$25,000</td>
<td>2025-02-10</td>
<td>2026-02-10</td>
<td>10.0%</td>
<td>450 ZDFZ</td>
<td><span class="badge bg-success">Active</span></td>
<td>
<button class="btn btn-sm btn-outline-success">Claim Rewards</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,281 @@
<div class="tab-pane fade" id="swap" role="tabpanel" aria-labelledby="swap-tab">
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<h5><i class="bi bi-info-circle"></i> About Swapping</h5>
<p>Swap allows you to exchange one token for another at the current market rate. Swaps are executed through liquidity pools with a small fee that goes to liquidity providers.</p>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mb-4">
<!-- Swap Card -->
<div class="card">
<div class="card-header">
<i class="bi bi-arrow-left-right me-1"></i> Swap Tokens
</div>
<div class="card-body">
<form action="/defi/swap" method="post">
<!-- From Token -->
<div class="mb-4">
<label class="form-label">From</label>
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="input-group">
<input type="number" class="form-control form-control-lg border-0" id="swapFromAmount" name="from_amount" placeholder="0.0" min="0" step="0.01">
<button class="btn btn-outline-secondary" type="button" id="maxFromButton">MAX</button>
</div>
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle d-flex align-items-center" type="button" id="fromTokenDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<span id="fromTokenSymbol">TFT</span>
</button>
<input type="hidden" name="from_token" id="fromTokenInput" value="TFT">
<ul class="dropdown-menu" aria-labelledby="fromTokenDropdown">
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="TFT" data-balance="10000">
<div>
<div>ThreeFold Token</div>
<small class="text-muted">Balance: 10,000 TFT</small>
</div>
</a></li>
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="ZDFZ" data-img="/static/img/tokens/zdfz.png" data-balance="5000">
<div>
<div>Zanzibar Token</div>
<small class="text-muted">Balance: 5,000 ZDFZ</small>
</div>
</a></li>
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="USDT" data-img="/static/img/tokens/usdt.png" data-balance="2500">
<div>
<div>Tether USD</div>
<small class="text-muted">Balance: 2,500 USDT</small>
</div>
</a></li>
</ul>
</div>
</div>
<div class="d-flex justify-content-between align-items-center text-muted small">
<span>Balance: <span id="fromTokenBalance">10,000 TFT</span></span>
<span>≈ $<span id="fromTokenUsdValue">5,000.00</span></span>
</div>
</div>
</div>
</div>
<!-- Swap Direction Button -->
<div class="d-flex justify-content-center mb-4">
<button type="button" class="btn btn-light rounded-circle p-2" id="swapDirectionButton">
<i class="bi bi-arrow-down-up fs-4"></i>
</button>
</div>
<!-- To Token -->
<div class="mb-4">
<label class="form-label">To (Estimated)</label>
<div class="card">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<input type="number" class="form-control form-control-lg border-0" id="swapToAmount" name="to_amount" placeholder="0.0" readonly>
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle d-flex align-items-center" type="button" id="toTokenDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
<span id="toTokenSymbol">ZDFZ</span>
</button>
<input type="hidden" name="to_token" id="toTokenInput" value="ZDFZ">
<ul class="dropdown-menu" aria-labelledby="toTokenDropdown">
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="TFT" data-img="/static/img/tokens/tft.png">
<div>ThreeFold Token</div>
</a></li>
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="ZDFZ" data-img="/static/img/tokens/zdfz.png">
<div>Zanzibar Token</div>
</a></li>
<li><a class="dropdown-item d-flex align-items-center" href="#" data-token="USDT" data-img="/static/img/tokens/usdt.png">
<div>Tether USD</div>
</a></li>
</ul>
</div>
</div>
<div class="d-flex justify-content-between align-items-center text-muted small">
<span>Balance: <span id="toTokenBalance">5,000 ZDFZ</span></span>
<span>≈ $<span id="toTokenUsdValue">2,500.00</span></span>
</div>
</div>
</div>
</div>
<!-- Exchange Rate Info -->
<div class="card mb-4">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center small">
<span>Exchange Rate:</span>
<span id="exchangeRate">1 TFT = 0.5 ZDFZ</span>
</div>
<div class="d-flex justify-content-between align-items-center small">
<span>Minimum Received:</span>
<span id="minimumReceived">0 ZDFZ</span>
</div>
<div class="d-flex justify-content-between align-items-center small">
<span>Price Impact:</span>
<span id="priceImpact" class="text-success">< 0.1%</span>
</div>
<div class="d-flex justify-content-between align-items-center small">
<span>Liquidity Provider Fee:</span>
<span id="lpFee">0.3%</span>
</div>
</div>
</div>
<!-- Swap Button -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary" id="swapButton">Swap</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<!-- Recent Swaps -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-clock-history me-1"></i> Recent Swaps
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Time</th>
<th>From</th>
<th>To</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>2025-04-15 14:32</td>
<td>
<div class="d-flex align-items-center">
500 TFT
</div>
</td>
<td>
<div class="d-flex align-items-center">
250 ZDFZ
</div>
</td>
<td>$250.00</td>
</tr>
<tr>
<td>2025-04-14 09:17</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
1,000 USDT
</div>
</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
2,000 TFT
</div>
</td>
<td>$1,000.00</td>
</tr>
<tr>
<td>2025-04-12 16:45</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
100 ZDFZ
</div>
</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
50 USDT
</div>
</td>
<td>$50.00</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Market Rates -->
<div class="card">
<div class="card-header">
<i class="bi bi-graph-up me-1"></i> Market Rates
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Pair</th>
<th>Rate</th>
<th>24h Change</th>
<th>Volume (24h)</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="position-relative me-2">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
</div>
TFT/ZDFZ
</div>
</td>
<td>0.5</td>
<td class="text-success">+2.3%</td>
<td>$125,000</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="position-relative me-2">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
</div>
TFT/USDT
</div>
</td>
<td>0.5</td>
<td class="text-danger">-1.2%</td>
<td>$250,000</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="position-relative me-2">
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
<i class="bi bi-coin me-2" style="font-size: 1.5rem;"></i>
</div>
ZDFZ/USDT
</div>
</td>
<td>0.5</td>
<td class="text-success">+3.7%</td>
<td>$175,000</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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 %}

View File

@@ -0,0 +1,411 @@
{% extends "base.html" %}
{% block title %}Flows Dashboard{% endblock %}
{% block content %}
<!-- Navigation Tabs -->
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="/flows">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/list">All Workflows</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/my-flows">My Workflows</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/create">Create Workflow</a>
</li>
</ul>
</div>
</div>
<!-- Info Alert -->
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Workflows</h5>
<p>The workflow system helps you track and manage business processes across your organization. Create new workflows, monitor progress, and collaborate with team members to ensure smooth operations.</p>
<div class="mt-2">
<a href="/flows/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
</div>
</div>
</div>
</div>
<!-- Dashboard Main Content -->
<div class="row mb-4">
<!-- Workflows with Pending Actions -->
<div class="col-lg-9 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Workflows with Pending Actions</h5>
<div>
<a href="/flows/pending" class="btn btn-sm btn-outline-primary">View All Pending</a>
</div>
</div>
<div class="card-body">
{% if flows and flows|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Workflow</th>
<th>Type</th>
<th>Current Step</th>
<th>Last Updated</th>
<th>Owner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="flex-shrink-0 me-2">
<div class="avatar bg-light text-primary rounded p-2">
<i class="bi bi-diagram-3"></i>
</div>
</div>
<div>
<a href="/flows/{{ flow.id }}" class="text-decoration-none fw-medium">{{ flow.name }}</a>
<div class="small text-muted">ID: {{ flow.id }}</div>
</div>
</div>
</td>
<td><span class="badge bg-info">{{ flow.flow_type }}</span></td>
<td>
{% if flow.current_step %}
<span class="text-warning fw-medium">{{ flow.current_step.name }}</span>
<div class="small text-muted">{{ flow.current_step.description }}</div>
{% else %}
<span class="text-muted">No pending step</span>
{% endif %}
</td>
<td>
<span>{{ flow.updated_at | date(format="%Y-%m-%d") }}</span>
{% if flow.status == 'Stuck' %}
<div class="small text-danger">May need attention</div>
{% else %}
<div class="small text-muted">Last updated</div>
{% endif %}
</td>
<td>
<div class="d-flex align-items-center">
<div class="flex-shrink-0 me-2">
<div class="avatar avatar-sm">
<img src="{{ flow.owner_avatar or '/static/img/avatar-placeholder.png' }}" alt="{{ flow.owner_name }}" class="rounded-circle" onerror="this.src='/static/img/avatar-placeholder.png'; this.onerror='';">
</div>
</div>
<div>{{ flow.owner_name }}</div>
</div>
</td>
<td>
<div class="d-flex gap-2">
<a href="/flows/{{ flow.id }}#take-action" class="btn btn-sm btn-primary">Take Action</a>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/flows/{{ flow.id }}">View Details</a></li>
<li><a class="dropdown-item" href="/flows/{{ flow.id }}/reassign">Reassign</a></li>
<li><a class="dropdown-item" href="/flows/{{ flow.id }}/extend">Extend Deadline</a></li>
</ul>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-check-circle-fill fs-1 text-success mb-3"></i>
<h5>No Pending Actions</h5>
<p class="text-muted">There are no workflows that require your immediate attention.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Timeline of Recent Flow Steps -->
<div class="col-lg-3 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Recent Activity</h5>
<a href="/flows/activity" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
{% if flows and flows|length > 0 %}
<div class="list-group list-group-flush">
{% set count = 0 %}
{% for flow in flows %}
{% if count < 8 %}
{% set count = count + 1 %}
<div class="list-group-item border-start-0 border-end-0 py-3">
<div class="d-flex">
<div class="me-3">
<div class="timeline-icon bg-light text-{% if flow.status == 'Completed' %}success{% elif flow.status == 'Stuck' %}danger{% else %}primary{% endif %} rounded-circle p-2">
<i class="bi bi-{% if flow.status == 'Completed' %}check-circle{% elif flow.status == 'Stuck' %}exclamation-triangle{% else %}arrow-right-circle{% endif %} fs-5"></i>
</div>
</div>
<div class="flex-fill">
<div class="d-flex justify-content-between align-items-center">
<div class="fw-medium">
{% if flow.status == 'In Progress' %}Working on{% elif flow.status == 'Completed' %}Completed{% elif flow.status == 'Stuck' %}Stuck at{% else %}Updated{% endif %}
{% if flow.current_step %} {{ flow.current_step.name }}{% endif %}
</div>
<div class="text-muted small">{{ flow.updated_at | date(format="%H:%M") }}</div>
</div>
<div>in <a href="/flows/{{ flow.id }}" class="text-decoration-none">{{ flow.name }}</a></div>
<div class="text-muted small mt-1">by {{ flow.owner_name }}</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="card-footer text-center">
<a href="/flows/activity" class="btn btn-sm btn-outline-secondary">View Full Activity Log</a>
</div>
{% else %}
<div class="list-group list-group-flush">
<div class="list-group-item border-start-0 border-end-0 py-5 text-center">
<i class="bi bi-hourglass fs-1 text-muted mb-3"></i>
<h6>No Recent Activity</h6>
<p class="text-muted small mb-0">Activity will appear here as workflows progress.</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Compact Filter Controls -->
<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">Workflow Filters</h5>
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="false" aria-controls="filterCollapse">
<i class="bi bi-funnel"></i> Show/Hide Filters
</button>
</div>
<div class="collapse show" id="filterCollapse">
<div class="card-body">
<form class="row g-3" action="/flows" method="get">
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="all" selected>All</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="stuck">Stuck</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<!-- Freezone filter - for UI demonstration only -->
<div class="col-md-3">
<label for="freezone" class="form-label">Freezone</label>
<select class="form-select" id="freezone" name="freezone" disabled>
<option value="all" selected>All Freezones</option>
<option value="dubai_multi_commodities_centre">DMCC</option>
<option value="dubai_international_financial_centre">DIFC</option>
<option value="jebel_ali_free_zone">JAFZA</option>
<option value="dubai_silicon_oasis">DSO</option>
<option value="dubai_internet_city">DIC</option>
<option value="dubai_media_city">DMC</option>
<option value="abu_dhabi_global_market">ADGM</option>
</select>
<div class="form-text">Coming soon</div>
</div>
<div class="col-md-3">
<label for="type" class="form-label">Workflow Type</label>
<select class="form-select" id="type" name="type">
<option value="all" selected>All</option>
<option value="company_registration">Company Incorporation</option>
<option value="user_onboarding">KYC Verification</option>
<option value="service_activation">License Activation</option>
<option value="payment_processing">Payment Processing</option>
</select>
</div>
<div class="col-md-3">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search" placeholder="Search workflows...">
</div>
<div class="col-12 text-end">
<button type="submit" class="btn btn-primary">
<i class="bi bi-filter me-1"></i> Apply Filters
</button>
<a href="/flows" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> Clear Filters
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Active Workflows Section -->
<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">Active Workflows (Recent Updates)</h5>
<a href="/flows/list" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body">
<div class="row">
{% set count = 0 %}
{% for flow in flows %}
{% if count < 3 and flow.status == 'In Progress' %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ flow.name }}</h5>
<h6 class="card-subtitle mb-2 text-muted">Owner: {{ flow.owner_name }}</h6>
<div class="mb-3">
<span class="badge bg-primary">{{ flow.flow_type }}</span>
</div>
<p class="mb-2">Current stage:
{% set current = flow.current_step %}
{% if current %}
{{ current.name }}
{% else %}
<span class="text-muted">No active stage</span>
{% endif %}
</p>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar bg-primary" role="progressbar"
style="width: {{ flow.progress_percentage }}%;"
aria-valuenow="{{ flow.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">{{ flow.progress_percentage }}%</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<small class="text-muted">Updated: {{ flow.updated_at | date(format="%Y-%m-%d") }}</small>
<a href="/flows/{{ flow.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
</div>
</div>
</div>
</div>
{% set count = count + 1 %}
{% endif %}
{% endfor %}
{% if count == 0 %}
<div class="col-12 text-center py-4">
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
<h5>No active workflows</h5>
<p class="text-muted">All workflows are either completed or not yet started.</p>
<a href="/flows/create" class="btn btn-primary mt-3">Create New Workflow</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Flows Table (Simplified) -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Recent Workflows</h5>
<a href="/flows/list" class="btn btn-sm btn-outline-primary">View All Workflows</a>
</div>
<div class="card-body">
{% if flows|length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Workflow Name</th>
<th>Type</th>
<th>Status</th>
<th>Assignee</th>
<th>Progress</th>
<th>Initiated</th>
<th>Last Updated</th>
<th>Current Stage</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>
<a href="/flows/{{ flow.id }}">{{ flow.name }}</a>
</td>
<td>{{ flow.flow_type }}</td>
<td>
<span
class="badge {% if flow.status == 'In Progress' %}bg-primary{% elif flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ flow.status }}
</span>
</td>
<td>{{ flow.owner_name }}</td>
<td>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar {% if flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-primary{% endif %}"
role="progressbar" style="width: {{ flow.progress_percentage }}%;"
aria-valuenow="{{ flow.progress_percentage }}" aria-valuemin="0"
aria-valuemax="100">{{ flow.progress_percentage }}%</div>
</div>
</td>
<td>{{ flow.created_at | date(format="%Y-%m-%d") }}</td>
<td>{{ flow.updated_at | date(format="%Y-%m-%d") }}</td>
<td>
{% set current = flow.current_step %}
{% if current %}
{{ current.name }}
{% else %}
{% if flow.status == 'Completed' %}
<span class="text-success">All stages completed</span>
{% elif flow.status == 'Cancelled' %}
<span class="text-secondary">Workflow cancelled</span>
{% else %}
<span class="text-muted">No active stage</span>
{% endif %}
{% endif %}
</td>
<td>
<div class="btn-group">
<a href="/flows/{{ flow.id }}" class="btn btn-sm btn-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
{% if flow.status == 'In Progress' %}
<a href="/flows/{{ flow.id }}#advance" class="btn btn-sm btn-success" title="Advance to Next Stage">
<i class="bi bi-arrow-right"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-search display-1 text-muted"></i>
<p class="lead mt-3">No workflows found matching your criteria.</p>
<p class="text-muted">Try adjusting your filters or create a new workflow.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,68 +3,266 @@
{% block title %}Flows Dashboard{% endblock %}
{% block content %}
<div class="row mb-4">
<!-- Navigation Tabs -->
<div class="row mb-3">
<div class="col-12">
<h1 class="display-5 mb-4">Flows Dashboard</h1>
<p class="lead">Track and manage workflow processes across the organization.</p>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="/flows">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/list">All Workflows</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/my-flows">My Workflows</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/flows/create">Create Workflow</a>
</li>
</ul>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<h5 class="card-title">Total Flows</h5>
<p class="display-4">{{ stats.total_flows }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<h5 class="card-title">In Progress</h5>
<p class="display-4">{{ stats.in_progress_flows }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body">
<h5 class="card-title">Stuck</h5>
<p class="display-4">{{ stats.stuck_flows }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-info h-100">
<div class="card-body">
<h5 class="card-title">Completed</h5>
<p class="display-4">{{ stats.completed_flows }}</p>
<!-- Info Alert -->
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Workflows</h5>
<p>The workflow system helps you track and manage business processes across your organization. Create new workflows, monitor progress, and collaborate with team members to ensure smooth operations.</p>
<div class="mt-2">
<a href="/flows/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<!-- Dashboard Main Content -->
<div class="row mb-4">
<!-- Workflows with Pending Actions -->
<div class="col-lg-9 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Workflows with Pending Actions</h5>
<div>
<a href="/flows/pending" class="btn btn-sm btn-outline-primary">View All Pending</a>
</div>
</div>
<div class="card-body">
{% if flows and flows|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Workflow</th>
<th>Type</th>
<th>Current Step</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="flex-shrink-0 me-2">
<div class="avatar bg-light text-primary rounded p-2">
<i class="bi bi-diagram-3"></i>
</div>
</div>
<div>
<a href="/flows/{{ flow.id }}" class="text-decoration-none fw-medium">{{ flow.name }}</a>
<div class="small text-muted">ID: {{ flow.id }}</div>
</div>
</div>
</td>
<td><span class="badge bg-info">{{ flow.flow_type }}</span></td>
<td>
{% if flow.current_step %}
<span class="text-warning fw-medium">{{ flow.current_step.name }}</span>
<div class="small text-muted">{{ flow.current_step.description }}</div>
{% else %}
<span class="text-muted">No pending step</span>
{% endif %}
</td>
<td>
<span>{{ flow.updated_at | date(format="%Y-%m-%d") }}</span>
{% if flow.status == 'Stuck' %}
<div class="small text-danger">May need attention</div>
{% else %}
<div class="small text-muted">Last updated</div>
{% endif %}
</td>
<td>
<div class="d-flex gap-2">
<a href="/flows/{{ flow.id }}#take-action" class="btn btn-sm btn-primary">Take Action</a>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-three-dots"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/flows/{{ flow.id }}">View Details</a></li>
<li><a class="dropdown-item" href="/flows/{{ flow.id }}/reassign">Reassign</a></li>
<li><a class="dropdown-item" href="/flows/{{ flow.id }}/extend">Extend Deadline</a></li>
</ul>
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-check-circle-fill fs-1 text-success mb-3"></i>
<h5>No Pending Actions</h5>
<p class="text-muted">There are no workflows that require your immediate attention.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Timeline of Recent Flow Steps -->
<div class="col-lg-3 mb-4">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Recent Activity</h5>
<a href="/flows/activity" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
{% if flows and flows|length > 0 %}
<div class="list-group list-group-flush">
{% set count = 0 %}
{% for flow in flows %}
{% if count < 8 %}
{% set count = count + 1 %}
<div class="list-group-item border-start-0 border-end-0 py-3">
<div class="d-flex">
<div class="me-3">
<div class="timeline-icon bg-light text-{% if flow.status == 'Completed' %}success{% elif flow.status == 'Stuck' %}danger{% else %}primary{% endif %} rounded-circle p-2">
<i class="bi bi-{% if flow.status == 'Completed' %}check-circle{% elif flow.status == 'Stuck' %}exclamation-triangle{% else %}arrow-right-circle{% endif %} fs-5"></i>
</div>
</div>
<div class="flex-fill">
<div class="d-flex justify-content-between align-items-center">
<div class="fw-medium">
{% if flow.status == 'In Progress' %}Working on{% elif flow.status == 'Completed' %}Completed{% elif flow.status == 'Stuck' %}Stuck at{% else %}Updated{% endif %}
{% if flow.current_step %} {{ flow.current_step.name }}{% endif %}
</div>
<div class="text-muted small">{{ flow.updated_at | date(format="%H:%M") }}</div>
</div>
<div>in <a href="/flows/{{ flow.id }}" class="text-decoration-none">{{ flow.name }}</a></div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="card-footer text-center">
<a href="/flows/activity" class="btn btn-sm btn-outline-secondary">View Full Activity Log</a>
</div>
{% else %}
<div class="list-group list-group-flush">
<div class="list-group-item border-start-0 border-end-0 py-5 text-center">
<i class="bi bi-hourglass fs-1 text-muted mb-3"></i>
<h6>No Recent Activity</h6>
<p class="text-muted small mb-0">Activity will appear here as workflows progress.</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Flows Table (Simplified) -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Quick Actions</h5>
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Recent Workflows</h5>
<a href="/flows/list" class="btn btn-sm btn-outline-primary">View All Workflows</a>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/flows/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Flow
</a>
<a href="/flows/list" class="btn btn-outline-secondary">
<i class="bi bi-list me-1"></i> View All Flows
</a>
<a href="/flows/my-flows" class="btn btn-outline-secondary">
<i class="bi bi-person me-1"></i> My Flows
</a>
{% if flows|length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Workflow Name</th>
<th>Type</th>
<th>Status</th>
<th>Progress</th>
<th>Initiated</th>
<th>Last Updated</th>
<th>Current Stage</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>
<a href="/flows/{{ flow.id }}">{{ flow.name }}</a>
</td>
<td>{{ flow.flow_type }}</td>
<td>
<span
class="badge {% if flow.status == 'In Progress' %}bg-primary{% elif flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ flow.status }}
</span>
</td>
<td>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar {% if flow.status == 'Completed' %}bg-success{% elif flow.status == 'Stuck' %}bg-danger{% else %}bg-primary{% endif %}"
role="progressbar" style="width: {{ flow.progress_percentage }}%;"
aria-valuenow="{{ flow.progress_percentage }}" aria-valuemin="0"
aria-valuemax="100">{{ flow.progress_percentage }}%</div>
</div>
</td>
<td>{{ flow.created_at | date(format="%Y-%m-%d") }}</td>
<td>{{ flow.updated_at | date(format="%Y-%m-%d") }}</td>
<td>
{% set current = flow.current_step %}
{% if current %}
{{ current.name }}
{% else %}
{% if flow.status == 'Completed' %}
<span class="text-success">All stages completed</span>
{% elif flow.status == 'Cancelled' %}
<span class="text-secondary">Workflow cancelled</span>
{% else %}
<span class="text-muted">No active stage</span>
{% endif %}
{% endif %}
</td>
<td>
<div class="btn-group">
<a href="/flows/{{ flow.id }}" class="btn btn-sm btn-primary" title="View Details">
<i class="bi bi-eye"></i>
</a>
{% if flow.status == 'In Progress' %}
<a href="/flows/{{ flow.id }}#advance" class="btn btn-sm btn-success" title="Advance to Next Stage">
<i class="bi bi-arrow-right"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-search display-1 text-muted"></i>
<p class="lead mt-3">No workflows found matching your criteria.</p>
<p class="text-muted">Try adjusting your filters or create a new workflow.</p>
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -3,51 +3,8 @@
{% block title %}Governance Dashboard{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Governance Dashboard</h1>
<p class="lead">Participate in the decision-making process by voting on proposals and creating new ones.</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<h5 class="card-title">Total Proposals</h5>
<p class="card-text display-6">{{ stats.total_proposals }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<h5 class="card-title">Active Proposals</h5>
<p class="card-text display-6">{{ stats.active_proposals }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-info h-100">
<div class="card-body">
<h5 class="card-title">Total Votes</h5>
<p class="card-text display-6">{{ stats.total_votes }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body">
<h5 class="card-title">Participation Rate</h5>
<p class="card-text display-6">{{ stats.participation_rate }}%</p>
</div>
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
@@ -66,43 +23,109 @@
</div>
</div>
<!-- Active Proposals Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<!-- Info Alert -->
<div class="row mb-2">
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Governance</h5>
<p>The governance system allows token holders to participate in decision-making processes by voting on proposals that affect the platform's future. Create proposals, cast votes, and help shape the direction of our decentralized ecosystem.</p>
<div class="mt-2">
<a href="/governance/documentation" class="btn btn-sm btn-outline-primary"><i class="bi bi-book"></i> Read Documentation</a>
</div>
</div>
</div>
</div>
<!-- Dashboard Main Content -->
<div class="row mb-3">
<!-- Voting Pane for Nearest Deadline Proposal -->
<div class="col-lg-8 mb-4 mb-lg-0">
{% if nearest_proposal is defined %}
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Active Proposals</h5>
<a href="/governance/proposals" class="btn btn-sm btn-outline-primary">View All</a>
<h5 class="mb-0">Urgent: Voting Closes Soon</h5>
<div>
<span class="badge bg-warning text-dark me-2">Ends: {{ nearest_proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
<a href="/governance/proposals/{{ nearest_proposal.id }}" class="btn btn-sm btn-outline-primary">View Full Proposal</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Title</th>
<th>Creator</th>
<th>Status</th>
<th>Voting Ends</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in proposals %}
{% if proposal.status == "Active" %}
<tr>
<td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td>
<td><span class="badge bg-success">{{ proposal.status }}</span></td>
<td>{{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<h4 class="card-title">{{ nearest_proposal.title }}</h4>
<h6 class="card-subtitle mb-3 text-muted">Proposed by {{ nearest_proposal.creator_name }}</h6>
<div class="mb-4">
<p>{{ nearest_proposal.description }}</p>
</div>
<div class="progress mb-3" style="height: 25px;">
<div class="progress-bar bg-success" role="progressbar" style="width: 65%" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">65% Yes</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: 35%" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">35% No</div>
</div>
<div class="d-flex justify-content-between text-muted small mb-4">
<span>26 votes cast</span>
<span>Quorum: 75% reached</span>
</div>
<div class="mb-4">
<h5 class="mb-3">Cast Your Vote</h5>
<form>
<div class="mb-3">
<input type="text" class="form-control" placeholder="Optional comment on your vote" aria-label="Vote comment">
</div>
<div class="d-flex justify-content-between">
<button type="submit" name="vote" value="yes" class="btn btn-success">Vote Yes</button>
<button type="submit" name="vote" value="no" class="btn btn-danger">Vote No</button>
<button type="submit" name="vote" value="abstain" class="btn btn-secondary">Abstain</button>
</div>
</form>
</div>
</div>
</div>
{% else %}
<div class="card h-100">
<div class="card-body text-center py-5">
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
<h5>No active proposals requiring votes</h5>
<p class="text-muted">When new proposals are created, they will appear here for voting.</p>
<a href="/governance/create" class="btn btn-primary mt-3">Create Proposal</a>
</div>
</div>
{% endif %}
</div>
<!-- Recent Activity Timeline -->
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Recent Activity</h5>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for activity in recent_activity %}
<div class="list-group-item border-start-0 border-end-0 py-3">
<div class="d-flex">
<div class="me-3">
<i class="bi {{ activity.icon }} fs-4"></i>
</div>
<div>
<div class="d-flex justify-content-between align-items-center">
<strong>{{ activity.user }}</strong>
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M") }}</small>
</div>
<p class="mb-1">{{ activity.action }} on <a href="/governance/proposals/{{ activity.proposal_id }}">{{ activity.proposal_title }}</a></p>
{% if activity.type == "comment" and activity.comment is defined %}
<p class="mb-0 small text-muted">"{{ activity.comment }}"</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="card-footer text-center">
<a href="/governance/proposals" class="btn btn-sm btn-outline-info">View All Activity</a>
</div>
</div>
</div>
@@ -113,7 +136,7 @@
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Recent Proposals</h5>
<h5 class="mb-0">Active Proposals (Ending Soon)</h5>
</div>
<div class="card-body">
<div class="row">
@@ -133,8 +156,8 @@
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
</div>
</div>
<div class="card-footer text-muted">
Created: {{ proposal.created_at | date(format="%Y-%m-%d") }}
<div class="card-footer text-muted text-center">
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
</div>
</div>
</div>
@@ -146,17 +169,4 @@
</div>
</div>
</div>
<!-- Call to Action -->
<div class="row mb-4">
<div class="col-12">
<div class="card bg-light">
<div class="card-body text-center">
<h4 class="mb-3">Have an idea to improve our platform?</h4>
<p class="mb-4">Create a proposal and let the community vote on it.</p>
<a href="/governance/create" class="btn btn-primary">Create Proposal</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -3,14 +3,6 @@
{% block title %}My Votes - Governance Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">My Votes</h1>
<p class="lead">View all proposals you have voted on.</p>
</div>
</div>
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="col-12">
@@ -52,7 +44,7 @@
</tr>
</thead>
<tbody>
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
<tr>
<td>{{ proposal.title }}</td>
<td>
@@ -96,7 +88,7 @@
<h5 class="card-title">Yes Votes</h5>
<p class="display-4">
{% set yes_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Yes' %}
{% set yes_count = yes_count + 1 %}
{% endif %}
@@ -112,7 +104,7 @@
<h5 class="card-title">No Votes</h5>
<p class="display-4">
{% set no_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'No' %}
{% set no_count = no_count + 1 %}
{% endif %}
@@ -128,7 +120,7 @@
<h5 class="card-title">Abstain Votes</h5>
<p class="display-4">
{% set abstain_count = 0 %}
{% for vote, proposal in votes %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Abstain' %}
{% set abstain_count = abstain_count + 1 %}
{% endif %}
@@ -140,5 +132,4 @@
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -3,14 +3,6 @@
{% block title %}Proposals - Governance Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Governance Proposals</h1>
<p class="lead">View and vote on all proposals in the system.</p>
</div>
</div>
<!-- Success message if present -->
{% if success %}
<div class="row mb-4">
@@ -24,7 +16,7 @@
{% endif %}
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
@@ -43,6 +35,17 @@
</div>
</div>
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
<div class="mt-2">
<a href="/governance/proposal-guidelines" class="btn btn-sm btn-outline-primary"><i class="bi bi-file-text"></i> Proposal Guidelines</a>
</div>
</div>
</div>
<!-- Filter Controls -->
<div class="row mb-4">
<div class="col-12">
@@ -124,5 +127,4 @@
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %}
{# Updated template with card blocks - 2025-04-22 #}
{% block title %}Home - Zanzibar Autonomous Zone{% endblock %}
{% block title %}Home - Zanzibar Digital Freezone{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title text-center mb-4">Zanzibar Autonomous Zone</h1>
<h1 class="card-title text-center mb-4">Zanzibar Digital Freezone</h1>
<p class="card-text text-center lead mb-5">Convenience, Safety and Privacy</p>
<style>

View File

@@ -0,0 +1,236 @@
{% extends "base.html" %}
{% block title %}Create Marketplace Listing{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Create New Listing</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
<li class="breadcrumb-item active">Create Listing</li>
</ol>
<div class="row">
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-plus-circle"></i>
Listing Details
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<form action="/marketplace/create" method="post">
<div class="mb-3">
<label for="title" class="form-label">Listing Title</label>
<input type="text" class="form-control" id="title" name="title" required>
<div class="form-text">A clear, descriptive title for your listing.</div>
</div>
<div class="mb-3">
<label for="asset_id" class="form-label">Select Asset</label>
<select class="form-select" id="asset_id" name="asset_id" required>
<option value="" selected disabled>Choose an asset to list</option>
{% for asset in assets %}
<option value="{{ asset.id }}" data-type="{{ asset.asset_type }}" data-image="{{ asset.image_url }}">
{{ asset.name }} ({{ asset.asset_type }})
</option>
{% endfor %}
</select>
<div class="form-text">Select one of your assets to list on the marketplace.</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="4" required></textarea>
<div class="form-text">Provide a detailed description of what you're selling.</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="price" class="form-label">Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="price" name="price" step="0.01" min="0.01" required>
</div>
<div class="form-text">Set a fair price for your asset.</div>
</div>
<div class="col-md-6">
<label for="currency" class="form-label">Currency</label>
<select class="form-select" id="currency" name="currency" required>
<option value="USD" selected>USD</option>
<option value="EUR">EUR</option>
<option value="BTC">BTC</option>
<option value="ETH">ETH</option>
<option value="TFT">TFT</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="listing_type" class="form-label">Listing Type</label>
<select class="form-select" id="listing_type" name="listing_type" required>
{% for type in listing_types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
<div class="form-text">Choose how you want to sell your asset.</div>
</div>
<div class="col-md-6">
<label for="duration_days" class="form-label">Duration (Days)</label>
<input type="number" class="form-control" id="duration_days" name="duration_days" min="1" max="90" value="30">
<div class="form-text">How long should this listing be active? (1-90 days)</div>
</div>
</div>
<div class="mb-3">
<label for="tags" class="form-label">Tags</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="digital, rare, collectible">
<div class="form-text">Comma-separated tags to help buyers find your listing.</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="terms" required>
<label class="form-check-label" for="terms">
I agree to the <a href="#" target="_blank">marketplace terms and conditions</a>.
</label>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/marketplace" class="btn btn-secondary me-md-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Listing</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Asset Preview -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-eye"></i>
Asset Preview
</div>
<div class="card-body text-center">
<div id="asset-preview-container">
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 200px;">
<i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
</div>
<p class="text-muted">Select an asset to preview</p>
</div>
</div>
</div>
<!-- Listing Tips -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-lightbulb"></i>
Listing Tips
</div>
<div class="card-body">
<ul class="list-unstyled">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
Use a clear, descriptive title
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
Include detailed information about your asset
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
Set a competitive price
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
Add relevant tags to improve discoverability
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
Choose the right listing type for your asset
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const assetSelect = document.getElementById('asset_id');
const previewContainer = document.getElementById('asset-preview-container');
const listingTypeSelect = document.getElementById('listing_type');
// Update preview when asset is selected
assetSelect.addEventListener('change', function() {
const selectedOption = assetSelect.options[assetSelect.selectedIndex];
const assetType = selectedOption.getAttribute('data-type');
const imageUrl = selectedOption.getAttribute('data-image');
const assetName = selectedOption.text;
let previewHtml = '';
if (imageUrl) {
previewHtml = `
<img src="${imageUrl}" class="img-fluid rounded mb-3" alt="${assetName}" style="max-height: 200px;">
`;
} else {
previewHtml = `
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 200px;">
<i class="bi bi-collection text-secondary" style="font-size: 3rem;"></i>
</div>
`;
}
previewHtml += `
<h5>${assetName}</h5>
<span class="badge bg-primary mb-2">${assetType}</span>
<p class="text-muted">This is how your asset will appear to buyers.</p>
`;
previewContainer.innerHTML = previewHtml;
// Suggest listing type based on asset type
if (assetType === 'Artwork') {
listingTypeSelect.value = 'Auction';
} else if (assetType === 'Token') {
listingTypeSelect.value = 'Fixed Price';
} else if (assetType === 'RealEstate') {
listingTypeSelect.value = 'Fixed Price';
}
});
// Show/hide duration field based on listing type
listingTypeSelect.addEventListener('change', function() {
const durationField = document.getElementById('duration_days');
const durationFieldParent = durationField.parentElement;
if (listingTypeSelect.value === 'Auction') {
durationFieldParent.style.display = 'block';
durationField.required = true;
if (!durationField.value) {
durationField.value = 7; // Default auction duration
}
} else if (listingTypeSelect.value === 'Exchange') {
durationFieldParent.style.display = 'block';
durationField.required = true;
if (!durationField.value) {
durationField.value = 30; // Default exchange duration
}
} else {
// For fixed price, duration is optional
durationFieldParent.style.display = 'block';
durationField.required = false;
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,293 @@
{% extends "base.html" %}
{% block title %}Digital Assets Marketplace{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Digital Assets Marketplace</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active">Marketplace</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.active_listings }}</h2>
<p class="mb-0">Active Listings</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
<div class="small text-white"><i class="bi bi-arrow-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 Market Value</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
<div class="small text-white"><i class="bi bi-arrow-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.total_listings }}</h2>
<p class="mb-0">Total Listings</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-info text-white mb-4">
<div class="card-body">
<h2 class="display-4">${{ stats.total_sales }}</h2>
<p class="mb-0">Total Sales</p>
</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="/marketplace/listings">View Details</a>
<div class="small text-white"><i class="bi bi-arrow-right"></i></div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-lightning-charge"></i>
Quick Actions
</div>
<div class="card-body">
<div class="d-flex justify-content-around">
<a href="/marketplace/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> List New Asset
</a>
<a href="/marketplace/listings" class="btn btn-success">
<i class="bi bi-search"></i> Browse Listings
</a>
<a href="/marketplace/my" class="btn btn-info">
<i class="bi bi-person"></i> My Listings
</a>
<a href="/assets/my" class="btn btn-secondary">
<i class="bi bi-collection"></i> My Assets
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Featured Listings -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-star"></i>
Featured Listings
</div>
<div class="card-body">
<div class="row">
{% if featured_listings|length > 0 %}
{% for listing in featured_listings %}
<div class="col-md-3 mb-3">
<div class="card h-100">
<div class="badge bg-warning text-dark position-absolute" style="top: 0.5rem; right: 0.5rem">Featured</div>
{% if listing.image_url %}
<img src="{{ listing.image_url }}" class="card-img-top" alt="{{ listing.title }}" style="height: 180px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 180px;">
<i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ listing.title }}</h5>
<p class="card-text text-truncate">{{ listing.description }}</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">{{ listing.listing_type }}</span>
<span class="badge bg-secondary">{{ listing.asset_type }}</span>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<strong>${{ listing.price }}</strong>
<a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<p class="text-center">No featured listings available at this time.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Recent Listings and Sales -->
<div class="row">
<!-- Recent Listings -->
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-clock"></i>
Recent Listings
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Type</th>
<th>Price</th>
<th>Listing Type</th>
<th>Seller</th>
<th>Listed</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% if recent_listings|length > 0 %}
{% for listing in recent_listings %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if listing.image_url %}
<img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<i class="bi bi-collection me-2"></i>
{% endif %}
{{ listing.asset_name }}
</div>
</td>
<td>
{% if listing.asset_type == "Token" %}
<span class="badge bg-primary">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "Artwork" %}
<span class="badge bg-info">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "RealEstate" %}
<span class="badge bg-success">Real Estate</span>
{% elif listing.asset_type == "IntellectualProperty" %}
<span class="badge bg-warning text-dark">IP</span>
{% else %}
<span class="badge bg-secondary">{{ listing.asset_type }}</span>
{% endif %}
</td>
<td>${{ listing.price }}</td>
<td>{{ listing.listing_type }}</td>
<td>{{ listing.seller_name }}</td>
<td>{{ listing.created_at|date }}</td>
<td>
<a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="7" class="text-center">No recent listings available.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<a href="/marketplace/listings" class="btn btn-sm btn-primary">View All Listings</a>
</div>
</div>
</div>
<!-- Recent Sales -->
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-bag-check"></i>
Recent Sales
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Price</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% if recent_sales|length > 0 %}
{% for listing in recent_sales %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if listing.image_url %}
<img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<i class="bi bi-collection me-2"></i>
{% endif %}
{{ listing.asset_name }}
</div>
</td>
<td>${{ listing.sale_price }}</td>
<td>{{ listing.sold_at|date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3" class="text-center">No recent sales available.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Listing Types Distribution -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-pie-chart"></i>
Listing Types
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Type</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{% for type, count in stats.listings_by_type %}
<tr>
<td>{{ type }}</td>
<td>{{ count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,356 @@
{% extends "base.html" %}
{% block title %}{{ listing.title }} | Marketplace{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Listing Details</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
<li class="breadcrumb-item"><a href="/marketplace/listings">Listings</a></li>
<li class="breadcrumb-item active">{{ listing.title }}</li>
</ol>
<!-- Listing Details -->
<div class="row">
<!-- Left Column: Image and Actions -->
<div class="col-md-5">
<div class="card mb-4">
<div class="card-body text-center">
{% if listing.image_url %}
<img src="{{ listing.image_url }}" alt="{{ listing.title }}" class="img-fluid rounded mb-3" style="max-height: 350px; object-fit: contain;">
{% else %}
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-3" style="height: 350px;">
<i class="bi bi-image text-secondary" style="font-size: 5rem;"></i>
</div>
{% endif %}
<div class="d-grid gap-2">
{% if listing.listing_type == "Fixed Price" %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#purchaseModal">
<i class="bi bi-cart"></i> Purchase Now
</button>
{% elif listing.listing_type == "Auction" %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#bidModal">
<i class="bi bi-hammer"></i> Place Bid
</button>
{% elif listing.listing_type == "Exchange" %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#offerModal">
<i class="bi bi-arrow-left-right"></i> Make Exchange Offer
</button>
{% endif %}
{% if listing.seller_id == user_id %}
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#cancelModal">
<i class="bi bi-x-circle"></i> Cancel Listing
</button>
{% endif %}
</div>
</div>
</div>
<!-- Asset Information -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-info-circle"></i>
Asset Information
</div>
<div class="card-body">
<p><strong>Asset Name:</strong> {{ listing.asset_name }}</p>
<p><strong>Asset Type:</strong>
{% if listing.asset_type == "Token" %}
<span class="badge bg-primary">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "Artwork" %}
<span class="badge bg-info">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "RealEstate" %}
<span class="badge bg-success">Real Estate</span>
{% elif listing.asset_type == "IntellectualProperty" %}
<span class="badge bg-warning text-dark">Intellectual Property</span>
{% else %}
<span class="badge bg-secondary">{{ listing.asset_type }}</span>
{% endif %}
</p>
<p><strong>Asset ID:</strong> <code>{{ listing.asset_id }}</code></p>
<a href="/assets/{{ listing.asset_id }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i> View Asset Details
</a>
</div>
</div>
</div>
<!-- Right Column: Details and Bids -->
<div class="col-md-7">
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="bi bi-tag"></i>
Listing Details
</div>
<div>
{% if listing.status == 'Active' %}
<span class="badge bg-success">{{ listing.status }}</span>
{% else %}
<span class="badge bg-secondary">{{ listing.status }}</span>
{% endif %}
</span>
</div>
</div>
</div>
<div class="card-body">
<h2 class="card-title mb-3">{{ listing.title }}</h2>
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<span class="badge bg-primary">{{ listing.listing_type }}</span>
{% if listing.featured %}
<span class="badge bg-warning text-dark">Featured</span>
{% endif %}
</div>
<div>
<h3 class="text-primary mb-0">${{ listing.price }}</h3>
</div>
</div>
<p class="card-text">{{ listing.description }}</p>
<hr>
<div class="row mb-3">
<div class="col-md-6">
<p><strong>Seller:</strong> {{ listing.seller_name }}</p>
<p><strong>Listed:</strong> {{ listing.created_at|date }}</p>
</div>
<div class="col-md-6">
<p><strong>Currency:</strong> {{ listing.currency }}</p>
<p><strong>Expires:</strong>
{% if listing.expires_at %}
{{ listing.expires_at|date }}
{% else %}
<span class="text-muted">No expiration</span>
{% endif %}
</p>
</div>
</div>
{% if listing.tags|length > 0 %}
<div class="mb-3">
<strong>Tags:</strong>
{% for tag in listing.tags %}
<span class="badge bg-secondary me-1">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
<!-- Bids Section (for Auctions) -->
{% if listing.listing_type == "Auction" %}
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-ol"></i>
Bids
</div>
<div class="card-body">
{% if listing.bids|length > 0 %}
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Bidder</th>
<th>Amount</th>
<th>Time</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for bid in listing.bids %}
<tr>
<td>{{ bid.bidder_name }}</td>
<td>${{ bid.amount }}</td>
<td>{{ bid.created_at|date }}</td>
<td>
{% if bid.status == 'Active' %}
<span class="badge bg-success">{{ bid.status }}</span>
{% else %}
<span class="badge bg-secondary">{{ bid.status }}</span>
{% endif %}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-2"><strong>Current Highest Bid:</strong> ${{ listing.highest_bid_amount }}</p>
{% else %}
<p>No bids yet. Be the first to bid!</p>
<p><strong>Starting Price:</strong> ${{ listing.price }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Similar Listings -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="bi bi-grid"></i>
Similar Listings
</div>
<div class="card-body">
<div class="row">
{% if similar_listings|length > 0 %}
{% for similar in similar_listings %}
<div class="col-md-3 mb-3">
<div class="card h-100">
{% if similar.image_url %}
<img src="{{ similar.image_url }}" class="card-img-top" alt="{{ similar.title }}" style="height: 150px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 150px;">
<i class="bi bi-image text-secondary" style="font-size: 2rem;"></i>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ similar.title }}</h5>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">{{ similar.listing_type }}</span>
<span class="badge bg-secondary">{{ similar.asset_type }}</span>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<strong>${{ similar.price }}</strong>
<a href="/marketplace/{{ similar.id }}" class="btn btn-sm btn-outline-primary">View</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<p class="text-center">No similar listings found.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Purchase Modal -->
<div class="modal fade" id="purchaseModal" tabindex="-1" aria-labelledby="purchaseModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="purchaseModalLabel">Purchase Asset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/marketplace/{{ listing.id }}/purchase" method="post">
<div class="modal-body">
<p>You are about to purchase <strong>{{ listing.asset_name }}</strong> for <strong>${{ listing.price }}</strong>.</p>
<div class="alert alert-info">
<h6>Purchase Details:</h6>
<ul>
<li>Asset: {{ listing.asset_name }}</li>
<li>Price: ${{ listing.price }} {{ listing.currency }}</li>
<li>Seller: {{ listing.seller_name }}</li>
</ul>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="agree-terms" name="agree_to_terms" required>
<label class="form-check-label" for="agree-terms">
I agree to the <a href="#" target="_blank">terms and conditions</a> of this purchase.
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Confirm Purchase</button>
</div>
</form>
</div>
</div>
</div>
<!-- Bid Modal -->
<div class="modal fade" id="bidModal" tabindex="-1" aria-labelledby="bidModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bidModalLabel">Place Bid</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/marketplace/{{ listing.id }}/bid" method="post">
<div class="modal-body">
<p>You are placing a bid on <strong>{{ listing.asset_name }}</strong>.</p>
<div class="alert alert-info">
<h6>Auction Details:</h6>
<ul>
<li>Asset: {{ listing.asset_name }}</li>
<li>Starting Price: ${{ listing.price }} {{ listing.currency }}</li>
{% if listing.highest_bid_amount %}
<li>Current Highest Bid: ${{ listing.highest_bid_amount }} {{ listing.currency }}</li>
<li>Minimum Bid: ${{ listing.highest_bid_amount + 1 }} {{ listing.currency }}</li>
{% else %}
<li>Minimum Bid: ${{ listing.price + 1 }} {{ listing.currency }}</li>
{% endif %}
</ul>
</div>
<div class="mb-3">
<label for="bid-amount" class="form-label">Your Bid Amount ({{ listing.currency }})</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" id="bid-amount" name="amount" step="0.01" min="{{ minimum_bid }}" required>
</div>
</div>
<input type="hidden" name="currency" value="{{ listing.currency }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Place Bid</button>
</div>
</form>
</div>
</div>
</div>
<!-- Cancel Modal -->
<div class="modal fade" id="cancelModal" tabindex="-1" aria-labelledby="cancelModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cancelModalLabel">Cancel Listing</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/marketplace/{{ listing.id }}/cancel" method="post">
<div class="modal-body">
<p>Are you sure you want to cancel this listing for <strong>{{ listing.asset_name }}</strong>?</p>
<div class="alert alert-warning">
<p>This action cannot be undone. The listing will be marked as cancelled and removed from the marketplace.</p>
{% if listing.bids|length > 0 %}
<p><strong>Note:</strong> This listing has active bids. Cancelling will notify all bidders.</p>
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No, Keep Listing</button>
<button type="submit" class="btn btn-danger">Yes, Cancel Listing</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,294 @@
{% extends "base.html" %}
{% block title %}Marketplace Listings{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">Marketplace Listings</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
<li class="breadcrumb-item active">Listings</li>
</ol>
<!-- Filters -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-funnel"></i>
Filter Listings
</div>
<div class="card-body">
<form id="filter-form" class="row g-3">
<div class="col-md-3">
<label for="asset-type" class="form-label">Asset Type</label>
<select id="asset-type" class="form-select">
<option value="">All Types</option>
{% for type in asset_types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="listing-type" class="form-label">Listing Type</label>
<select id="listing-type" class="form-select">
<option value="">All Listings</option>
{% for type in listing_types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="price-min" class="form-label">Min Price</label>
<input type="number" class="form-control" id="price-min" placeholder="Min $">
</div>
<div class="col-md-3">
<label for="price-max" class="form-label">Max Price</label>
<input type="number" class="form-control" id="price-max" placeholder="Max $">
</div>
<div class="col-12">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" placeholder="Search by name, description, or tags">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Apply Filters</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</form>
</div>
</div>
<!-- View Toggle -->
<div class="mb-3">
<div class="btn-group" role="group" aria-label="View Toggle">
<button type="button" class="btn btn-outline-primary active" id="grid-view-btn">
<i class="bi bi-grid"></i> Grid View
</button>
<button type="button" class="btn btn-outline-primary" id="list-view-btn">
<i class="bi bi-list"></i> List View
</button>
</div>
<a href="/marketplace/create" class="btn btn-success float-end">
<i class="bi bi-plus-circle"></i> List New Asset
</a>
</div>
<!-- Grid View -->
<div id="grid-view">
<div class="row">
{% if listings|length > 0 %}
{% for listing in listings %}
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 listing-item"
data-asset-type="{{ listing.asset_type }}"
data-listing-type="{{ listing.listing_type }}"
data-price="{{ listing.price }}">
<div class="card h-100">
{% if listing.featured %}
<div class="badge bg-warning text-dark position-absolute" style="top: 0.5rem; right: 0.5rem">Featured</div>
{% endif %}
{% if listing.image_url %}
<img src="{{ listing.image_url }}" class="card-img-top" alt="{{ listing.title }}" style="height: 180px; object-fit: cover;">
{% else %}
<div class="card-img-top bg-light d-flex align-items-center justify-content-center" style="height: 180px;">
<i class="bi bi-image text-secondary" style="font-size: 3rem;"></i>
</div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ listing.title }}</h5>
<p class="card-text text-truncate">{{ listing.description }}</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-primary">{{ listing.listing_type }}</span>
{% if listing.asset_type == "Token" %}
<span class="badge bg-primary">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "Artwork" %}
<span class="badge bg-info">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "RealEstate" %}
<span class="badge bg-success">Real Estate</span>
{% elif listing.asset_type == "IntellectualProperty" %}
<span class="badge bg-warning text-dark">IP</span>
{% else %}
<span class="badge bg-secondary">{{ listing.asset_type }}</span>
{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">Listed by {{ listing.seller_name }}</small>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<strong>${{ listing.price }}</strong>
<a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="alert alert-info">
No listings found. <a href="/marketplace/create">Create a new listing</a>
</div>
</div>
{% endif %}
</div>
</div>
<!-- List View -->
<div id="list-view" style="display: none;">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-ul"></i>
All Listings
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Title</th>
<th>Type</th>
<th>Price</th>
<th>Listing Type</th>
<th>Seller</th>
<th>Listed</th>
<th>Expires</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% if listings|length > 0 %}
{% for listing in listings %}
<tr class="listing-item"
data-asset-type="{{ listing.asset_type }}"
data-listing-type="{{ listing.listing_type }}"
data-price="{{ listing.price }}">
<td>
<div class="d-flex align-items-center">
{% if listing.image_url %}
<img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<i class="bi bi-collection me-2"></i>
{% endif %}
</div>
</td>
<td>{{ listing.title }}</td>
<td>
{% if listing.asset_type == "Token" %}
<span class="badge bg-primary">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "Artwork" %}
<span class="badge bg-info">{{ listing.asset_type }}</span>
{% elif listing.asset_type == "RealEstate" %}
<span class="badge bg-success">Real Estate</span>
{% elif listing.asset_type == "IntellectualProperty" %}
<span class="badge bg-warning text-dark">IP</span>
{% else %}
<span class="badge bg-secondary">{{ listing.asset_type }}</span>
{% endif %}
</td>
<td>${{ listing.price }}</td>
<td>{{ listing.listing_type }}</td>
<td>{{ listing.seller_name }}</td>
<td>{{ listing.created_at|date }}</td>
<td>
{% if listing.expires_at %}
{{ listing.expires_at|date }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
<a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">View</a>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="9" class="text-center">No listings available.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// View toggle
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
const gridView = document.getElementById('grid-view');
const listView = document.getElementById('list-view');
gridViewBtn.addEventListener('click', function() {
gridView.style.display = 'block';
listView.style.display = 'none';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
});
listViewBtn.addEventListener('click', function() {
gridView.style.display = 'none';
listView.style.display = 'block';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
});
// Filtering
const filterForm = document.getElementById('filter-form');
const assetTypeSelect = document.getElementById('asset-type');
const listingTypeSelect = document.getElementById('listing-type');
const priceMinInput = document.getElementById('price-min');
const priceMaxInput = document.getElementById('price-max');
const searchInput = document.getElementById('search');
const listingItems = document.querySelectorAll('.listing-item');
filterForm.addEventListener('submit', function(e) {
e.preventDefault();
applyFilters();
});
filterForm.addEventListener('reset', function() {
setTimeout(function() {
applyFilters();
}, 10);
});
function applyFilters() {
const assetType = assetTypeSelect.value;
const listingType = listingTypeSelect.value;
const priceMin = priceMinInput.value ? parseFloat(priceMinInput.value) : 0;
const priceMax = priceMaxInput.value ? parseFloat(priceMaxInput.value) : Infinity;
const searchTerm = searchInput.value.toLowerCase();
listingItems.forEach(function(item) {
const itemAssetType = item.getAttribute('data-asset-type');
const itemListingType = item.getAttribute('data-listing-type');
const itemPrice = parseFloat(item.getAttribute('data-price'));
const itemTitle = item.querySelector('.card-title') ?
item.querySelector('.card-title').textContent.toLowerCase() : '';
const itemDescription = item.querySelector('.card-text') ?
item.querySelector('.card-text').textContent.toLowerCase() : '';
const assetTypeMatch = !assetType || itemAssetType === assetType;
const listingTypeMatch = !listingType || itemListingType === listingType;
const priceMatch = itemPrice >= priceMin && itemPrice <= priceMax;
const searchMatch = !searchTerm ||
itemTitle.includes(searchTerm) ||
itemDescription.includes(searchTerm);
if (assetTypeMatch && listingTypeMatch && priceMatch && searchMatch) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,238 @@
{% extends "base.html" %}
{% block title %}My Marketplace Listings{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<h1 class="mt-4">My Listings</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="/marketplace">Marketplace</a></li>
<li class="breadcrumb-item active">My Listings</li>
</ol>
<div class="row mb-3">
<div class="col-12">
<a href="/marketplace/create" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Create New Listing
</a>
</div>
</div>
<!-- Listings Table -->
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-list-ul"></i>
My Listings
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Asset</th>
<th>Title</th>
<th>Price</th>
<th>Type</th>
<th>Status</th>
<th>Created</th>
<th>Expires</th>
<th>Views</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if listings|length > 0 %}
{% for listing in listings %}
<tr>
<td>
<div class="d-flex align-items-center">
{% if listing.image_url %}
<img src="{{ listing.image_url }}" alt="{{ listing.asset_name }}" class="me-2" style="width: 30px; height: 30px; object-fit: cover;">
{% else %}
<i class="bi bi-collection me-2"></i>
{% endif %}
{{ listing.asset_name }}
</div>
</td>
<td>{{ listing.title }}</td>
<td>${{ listing.price }}</td>
<td>
<span class="badge bg-primary">{{ listing.listing_type }}</span>
</td>
<td>
{% if listing.status == "Active" %}
<span class="badge bg-success">{{ listing.status }}</span>
{% elif listing.status == "Sold" %}
<span class="badge bg-info">{{ listing.status }}</span>
{% elif listing.status == "Cancelled" %}
<span class="badge bg-danger">{{ listing.status }}</span>
{% elif listing.status == "Expired" %}
<span class="badge bg-warning text-dark">{{ listing.status }}</span>
{% endif %}
</td>
<td>{{ listing.created_at|date }}</td>
<td>
{% if listing.expires_at %}
{{ listing.expires_at|date }}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ listing.views }}</td>
<td>
<div class="btn-group" role="group">
<a href="/marketplace/{{ listing.id }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
{% if listing.status == "Active" %}
<form action="/marketplace/{{ listing.id }}/cancel" method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to cancel this listing?')">
<i class="bi bi-x-circle"></i>
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="9" class="text-center">
You don't have any listings yet.
<a href="/marketplace/create">Create your first listing</a>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Listing Statistics -->
<div class="row">
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-bar-chart"></i>
Listings by Status
</div>
<div class="card-body">
<canvas id="statusChart" width="100%" height="50"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-pie-chart"></i>
Listings by Type
</div>
<div class="card-body">
<canvas id="typeChart" width="100%" height="50"></canvas>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Count listings by status
const listingsData = JSON.parse('{{ listings|tojson|safe }}');
const statusCounts = {
'Active': 0,
'Sold': 0,
'Cancelled': 0,
'Expired': 0
};
const typeCounts = {
'Fixed Price': 0,
'Auction': 0,
'Exchange': 0
};
listingsData.forEach(listing => {
statusCounts[listing.status] += 1;
typeCounts[listing.listing_type] += 1;
});
// Status Chart
const statusCtx = document.getElementById('statusChart').getContext('2d');
new Chart(statusCtx, {
type: 'bar',
data: {
labels: Object.keys(statusCounts),
datasets: [{
label: 'Number of Listings',
data: Object.values(statusCounts),
backgroundColor: [
'rgba(40, 167, 69, 0.7)', // Active - green
'rgba(23, 162, 184, 0.7)', // Sold - cyan
'rgba(220, 53, 69, 0.7)', // Cancelled - red
'rgba(255, 193, 7, 0.7)' // Expired - yellow
],
borderColor: [
'rgba(40, 167, 69, 1)',
'rgba(23, 162, 184, 1)',
'rgba(220, 53, 69, 1)',
'rgba(255, 193, 7, 1)'
],
borderWidth: 1
}]
},
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
// Type Chart
const typeCtx = document.getElementById('typeChart').getContext('2d');
new Chart(typeCtx, {
type: 'pie',
data: {
labels: Object.keys(typeCounts),
datasets: [{
data: Object.values(typeCounts),
backgroundColor: [
'rgba(0, 123, 255, 0.7)', // Fixed Price - blue
'rgba(111, 66, 193, 0.7)', // Auction - purple
'rgba(23, 162, 184, 0.7)' // Exchange - cyan
],
borderColor: [
'rgba(0, 123, 255, 1)',
'rgba(111, 66, 193, 1)',
'rgba(23, 162, 184, 1)'
],
borderWidth: 1
}]
},
options: {
plugins: {
legend: {
position: 'right'
}
}
}
});
});
</script>
{% endblock %}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,173 @@
// Company data (would be loaded from backend in production)
var companyData = {
'company1': {
name: 'Zanzibar Digital Solutions',
type: 'Startup FZC',
status: 'Active',
registrationDate: '2025-04-01',
purpose: 'Digital solutions and blockchain development',
plan: 'Startup FZC - $50/month',
nextBilling: '2025-06-01',
paymentMethod: 'Credit Card (****4582)',
shareholders: [
{ name: 'John Smith', percentage: '60%' },
{ name: 'Sarah Johnson', percentage: '40%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' }
]
},
'company2': {
name: 'Blockchain Innovations Ltd',
type: 'Growth FZC',
status: 'Active',
registrationDate: '2025-03-15',
purpose: 'Blockchain technology research and development',
plan: 'Growth FZC - $100/month',
nextBilling: '2025-06-15',
paymentMethod: 'Bank Transfer',
shareholders: [
{ name: 'Michael Chen', percentage: '35%' },
{ name: 'Aisha Patel', percentage: '35%' },
{ name: 'David Okonkwo', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Digital Asset Issuance', status: 'Signed' },
{ name: 'Physical Asset Holding', status: 'Signed' }
]
},
'company3': {
name: 'Sustainable Energy Cooperative',
type: 'Cooperative FZC',
status: 'Pending',
registrationDate: '2025-05-01',
purpose: 'Renewable energy production and distribution',
plan: 'Cooperative FZC - $200/month',
nextBilling: 'Pending Activation',
paymentMethod: 'Pending',
shareholders: [
{ name: 'Community Energy Group', percentage: '40%' },
{ name: 'Green Future Initiative', percentage: '30%' },
{ name: 'Sustainable Living Collective', percentage: '30%' }
],
contracts: [
{ name: 'Articles of Incorporation', status: 'Signed' },
{ name: 'Terms & Conditions', status: 'Signed' },
{ name: 'Cooperative Governance', status: 'Pending' }
]
}
};
// Current company ID for modal
var currentCompanyId = null;
// View company details function
function viewCompanyDetails(companyId) {
// Store current company ID
currentCompanyId = companyId;
// Get company data
const company = companyData[companyId];
if (!company) return;
// Update modal title
document.getElementById('companyDetailsModalLabel').innerHTML =
`<i class="bi bi-building me-2"></i>${company.name} Details`;
// Update general information
document.getElementById('modal-company-name').textContent = company.name;
document.getElementById('modal-company-type').textContent = company.type;
document.getElementById('modal-registration-date').textContent = company.registrationDate;
// Update status with appropriate badge
const statusBadge = company.status === 'Active' ?
`<span class="badge bg-success">${company.status}</span>` :
`<span class="badge bg-warning text-dark">${company.status}</span>`;
document.getElementById('modal-status').innerHTML = statusBadge;
document.getElementById('modal-purpose').textContent = company.purpose;
// Update billing information
document.getElementById('modal-plan').textContent = company.plan;
document.getElementById('modal-next-billing').textContent = company.nextBilling;
document.getElementById('modal-payment-method').textContent = company.paymentMethod;
// Update shareholders table
const shareholdersTable = document.getElementById('modal-shareholders');
shareholdersTable.innerHTML = '';
company.shareholders.forEach(shareholder => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${shareholder.name}</td>
<td>${shareholder.percentage}</td>
`;
shareholdersTable.appendChild(row);
});
// Update contracts table
const contractsTable = document.getElementById('modal-contracts');
contractsTable.innerHTML = '';
company.contracts.forEach(contract => {
const row = document.createElement('tr');
const statusBadge = contract.status === 'Signed' ?
`<span class="badge bg-success">${contract.status}</span>` :
`<span class="badge bg-warning text-dark">${contract.status}</span>`;
row.innerHTML = `
<td>${contract.name}</td>
<td>${statusBadge}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="viewContract('${contract.name.toLowerCase().replace(/\s+/g, '-')}')">View</button></td>
`;
contractsTable.appendChild(row);
});
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('companyDetailsModal'));
modal.show();
}
// Switch to entity function
function switchToEntity(companyId) {
const company = companyData[companyId];
if (!company) return;
// In a real application, this would redirect to the entity context
// For now, we'll just show an alert
alert(`Switching to ${company.name} entity context. All UI will now reflect this entity's governance, billing, and other features.`);
// This would typically involve:
// 1. Setting a session/cookie for the current entity
// 2. Redirecting to the dashboard with that entity context
// window.location.href = `/dashboard?entity=${companyId}`;
}
// Switch to entity from modal
function switchToEntityFromModal() {
if (currentCompanyId) {
switchToEntity(currentCompanyId);
// Close the modal
const modal = bootstrap.Modal.getInstance(document.getElementById('companyDetailsModal'));
modal.hide();
}
}
// View contract function
function viewContract(contractId) {
// In a real application, this would open the contract document
// For now, we'll just show an alert
alert(`Viewing contract: ${contractId.replace(/-/g, ' ')}`);
// This would typically involve:
// 1. Fetching the contract document from the server
// 2. Opening it in a viewer or new tab
// window.open(`/contracts/view/${contractId}`, '_blank');
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Company management script loaded');
});

View File

@@ -0,0 +1,562 @@
// DeFi Platform JavaScript Functionality
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// =============== LENDING & BORROWING TAB ===============
// Lending form calculations
const lendingAmountInput = document.getElementById('lendingAmount');
const lendingAssetSelect = document.getElementById('lendingAsset');
const lendingTermSelect = document.getElementById('lendingTerm');
const estimatedReturnsElement = document.getElementById('estimatedReturns');
const totalReturnElement = document.getElementById('totalReturn');
if (lendingAmountInput && lendingAssetSelect && lendingTermSelect) {
const calculateLendingReturns = () => {
const amount = parseFloat(lendingAmountInput.value) || 0;
const asset = lendingAssetSelect.value;
const termDays = parseInt(lendingTermSelect.value) || 30;
// Get APY from the selected option's text
const selectedOption = lendingTermSelect.options[lendingTermSelect.selectedIndex];
const apyMatch = selectedOption.text.match(/\((\d+\.\d+)%\)/);
const apy = apyMatch ? parseFloat(apyMatch[1]) / 100 : 0.05; // Default to 5% if not found
// Calculate returns (simple interest for demonstration)
const returns = amount * apy * (termDays / 365);
const total = amount + returns;
if (estimatedReturnsElement) {
estimatedReturnsElement.textContent = returns.toFixed(2) + ' ' + asset;
}
if (totalReturnElement) {
totalReturnElement.textContent = total.toFixed(2) + ' ' + asset;
}
};
lendingAmountInput.addEventListener('input', calculateLendingReturns);
lendingAssetSelect.addEventListener('change', calculateLendingReturns);
lendingTermSelect.addEventListener('change', calculateLendingReturns);
}
// Borrowing form calculations
const borrowingAmountInput = document.getElementById('borrowingAmount');
const borrowingAssetSelect = document.getElementById('borrowingAsset');
const borrowingTermSelect = document.getElementById('borrowingTerm');
const borrowingCollateralSelect = document.getElementById('collateralAsset');
const borrowingCollateralAmountInput = document.getElementById('collateralAmount');
const interestDueElement = document.getElementById('interestDue');
const totalRepaymentElement = document.getElementById('totalRepayment');
const borrowingCollateralRatioElement = document.getElementById('collateralRatio');
if (borrowingAmountInput && borrowingAssetSelect && borrowingCollateralSelect && borrowingCollateralAmountInput) {
const calculateBorrowingDetails = () => {
const amount = parseFloat(borrowingAmountInput.value) || 0;
const asset = borrowingAssetSelect.value;
const termDays = parseInt(borrowingTermSelect.value) || 30;
// Get APR from the selected option's text
const selectedOption = borrowingTermSelect.options[borrowingTermSelect.selectedIndex];
const aprMatch = selectedOption.text.match(/\((\d+\.\d+)%\)/);
const apr = aprMatch ? parseFloat(aprMatch[1]) / 100 : 0.08; // Default to 8% if not found
// Calculate interest and total repayment
const interest = amount * apr * (termDays / 365);
const total = amount + interest;
if (interestDueElement) {
interestDueElement.textContent = interest.toFixed(2) + ' ' + asset;
}
if (totalRepaymentElement) {
totalRepaymentElement.textContent = total.toFixed(2) + ' ' + asset;
}
// Calculate collateral ratio
const collateralAmount = parseFloat(borrowingCollateralAmountInput.value) || 0;
const collateralAsset = borrowingCollateralSelect.value;
let collateralValue = 0;
// Mock prices for demonstration
const assetPrices = {
'TFT': 0.5,
'ZDFZ': 0.5,
'USDT': 1.0
};
if (collateralAsset in assetPrices) {
collateralValue = collateralAmount * assetPrices[collateralAsset];
} else {
// For other assets, assume the value is the amount (simplified)
collateralValue = collateralAmount;
}
const borrowValue = amount * (asset === 'USDT' ? 1 : assetPrices[asset] || 0.5);
const ratio = borrowValue > 0 ? (collateralValue / borrowValue * 100) : 0;
if (borrowingCollateralRatioElement) {
borrowingCollateralRatioElement.textContent = ratio.toFixed(0) + '%';
// Update color based on ratio
if (ratio >= 200) {
borrowingCollateralRatioElement.className = 'text-success';
} else if (ratio >= 150) {
borrowingCollateralRatioElement.className = 'text-warning';
} else {
borrowingCollateralRatioElement.className = 'text-danger';
}
}
};
borrowingAmountInput.addEventListener('input', calculateBorrowingDetails);
borrowingAssetSelect.addEventListener('change', calculateBorrowingDetails);
borrowingTermSelect.addEventListener('change', calculateBorrowingDetails);
borrowingCollateralSelect.addEventListener('change', calculateBorrowingDetails);
borrowingCollateralAmountInput.addEventListener('input', calculateBorrowingDetails);
}
// =============== LIQUIDITY POOLS TAB ===============
// Add Liquidity form calculations
const poolSelect = document.getElementById('liquidityPool');
const token1AmountInput = document.getElementById('token1Amount');
const token2AmountInput = document.getElementById('token2Amount');
const lpTokensElement = document.getElementById('lpTokensReceived');
const poolShareElement = document.getElementById('poolShare');
if (poolSelect && token1AmountInput && token2AmountInput) {
const calculateLiquidityDetails = () => {
const token1Amount = parseFloat(token1AmountInput.value) || 0;
const token2Amount = parseFloat(token2AmountInput.value) || 0;
// Mock calculations for demonstration
const lpTokens = Math.sqrt(token1Amount * token2Amount);
const poolShare = token1Amount > 0 ? (lpTokens / (lpTokens + 1000) * 100) : 0;
if (lpTokensElement) {
lpTokensElement.textContent = lpTokens.toFixed(2);
}
if (poolShareElement) {
poolShareElement.textContent = poolShare.toFixed(2) + '%';
}
};
token1AmountInput.addEventListener('input', calculateLiquidityDetails);
token2AmountInput.addEventListener('input', calculateLiquidityDetails);
// Handle pool selection to update token labels
poolSelect.addEventListener('change', function() {
const selectedOption = poolSelect.options[poolSelect.selectedIndex];
const token1Label = document.getElementById('token1Label');
const token2Label = document.getElementById('token2Label');
if (selectedOption.value === 'tft-zdfz') {
if (token1Label) token1Label.textContent = 'TFT';
if (token2Label) token2Label.textContent = 'ZDFZ';
} else if (selectedOption.value === 'zdfz-usdt') {
if (token1Label) token1Label.textContent = 'ZDFZ';
if (token2Label) token2Label.textContent = 'USDT';
}
calculateLiquidityDetails();
});
}
// =============== STAKING TAB ===============
// TFT Staking calculations
const tftStakeAmountInput = document.getElementById('tftStakeAmount');
const tftStakingPeriodSelect = document.getElementById('tftStakingPeriod');
const tftEstimatedRewardsElement = document.getElementById('tftEstimatedRewards');
if (tftStakeAmountInput && tftStakingPeriodSelect && tftEstimatedRewardsElement) {
const calculateTftStakingRewards = () => {
const amount = parseFloat(tftStakeAmountInput.value) || 0;
const termDays = parseInt(tftStakingPeriodSelect.value) || 30;
// Get APY from the selected option's text
const selectedOption = tftStakingPeriodSelect.options[tftStakingPeriodSelect.selectedIndex];
const apyMatch = selectedOption.text.match(/\((\d+\.\d+)%\)/);
const apy = apyMatch ? parseFloat(apyMatch[1]) / 100 : 0.085; // Default to 8.5% if not found
// Calculate rewards (simple interest for demonstration)
const rewards = amount * apy * (termDays / 365);
tftEstimatedRewardsElement.textContent = rewards.toFixed(2) + ' TFT';
};
tftStakeAmountInput.addEventListener('input', calculateTftStakingRewards);
tftStakingPeriodSelect.addEventListener('change', calculateTftStakingRewards);
}
// ZDFZ Staking calculations
const zazStakeAmountInput = document.getElementById('zazStakeAmount');
const zazStakingPeriodSelect = document.getElementById('zazStakingPeriod');
const zazEstimatedRewardsElement = document.getElementById('zazEstimatedRewards');
if (zazStakeAmountInput && zazStakingPeriodSelect && zazEstimatedRewardsElement) {
const calculateZazStakingRewards = () => {
const amount = parseFloat(zazStakeAmountInput.value) || 0;
const termDays = parseInt(zazStakingPeriodSelect.value) || 30;
// Get APY from the selected option's text
const selectedOption = zazStakingPeriodSelect.options[zazStakingPeriodSelect.selectedIndex];
const apyMatch = selectedOption.text.match(/\((\d+\.\d+)%\)/);
const apy = apyMatch ? parseFloat(apyMatch[1]) / 100 : 0.12; // Default to 12% if not found
// Calculate rewards (simple interest for demonstration)
const rewards = amount * apy * (termDays / 365);
zazEstimatedRewardsElement.textContent = rewards.toFixed(2) + ' ZDFZ';
};
zazStakeAmountInput.addEventListener('input', calculateZazStakingRewards);
zazStakingPeriodSelect.addEventListener('change', calculateZazStakingRewards);
}
// Asset Staking calculations
const assetStakingSelect = document.getElementById('assetStaking');
const assetStakingPeriodSelect = document.getElementById('assetStakingPeriod');
const assetEstimatedRewardsElement = document.getElementById('assetEstimatedRewards');
if (assetStakingSelect && assetStakingPeriodSelect && assetEstimatedRewardsElement) {
const calculateAssetStakingRewards = () => {
const selectedOption = assetStakingSelect.options[assetStakingSelect.selectedIndex];
if (selectedOption.value === '') return;
const assetValue = parseFloat(selectedOption.dataset.value) || 0;
const termDays = parseInt(assetStakingPeriodSelect.value) || 30;
// Get APY from the selected option's text
const periodOption = assetStakingPeriodSelect.options[assetStakingPeriodSelect.selectedIndex];
const apyMatch = periodOption.text.match(/\((\d+\.\d+)%\)/);
const apy = apyMatch ? parseFloat(apyMatch[1]) / 100 : 0.035; // Default to 3.5% if not found
// Calculate rewards in USD (simple interest for demonstration)
const rewards = assetValue * apy * (termDays / 365);
assetEstimatedRewardsElement.textContent = '$' + rewards.toFixed(2);
};
assetStakingSelect.addEventListener('change', calculateAssetStakingRewards);
assetStakingPeriodSelect.addEventListener('change', calculateAssetStakingRewards);
}
// =============== SWAP TAB ===============
// Token swap calculations
const swapFromAmountInput = document.getElementById('swapFromAmount');
const swapToAmountElement = document.getElementById('swapToAmount');
const fromTokenDropdown = document.getElementById('fromTokenDropdown');
const toTokenDropdown = document.getElementById('toTokenDropdown');
const exchangeRateElement = document.getElementById('exchangeRate');
const minimumReceivedElement = document.getElementById('minimumReceived');
const priceImpactElement = document.getElementById('priceImpact');
const swapDirectionButton = document.getElementById('swapDirectionButton');
const maxFromButton = document.getElementById('maxFromButton');
const fromTokenSymbolElement = document.getElementById('fromTokenSymbol');
const toTokenSymbolElement = document.getElementById('toTokenSymbol');
const fromTokenImgElement = document.getElementById('fromTokenImg');
const toTokenImgElement = document.getElementById('toTokenImg');
const fromTokenBalanceElement = document.getElementById('fromTokenBalance');
const toTokenBalanceElement = document.getElementById('toTokenBalance');
// Mock token data
const tokenData = {
'TFT': { price: 0.5, balance: '10,000 TFT', usdValue: '5,000.00' },
'ZDFZ': { price: 0.5, balance: '5,000 ZDFZ', usdValue: '2,500.00' },
'USDT': { price: 1.0, balance: '2,500 USDT', usdValue: '2,500.00' }
};
if (swapFromAmountInput && swapToAmountElement) {
let fromToken = 'TFT';
let toToken = 'ZDFZ';
const calculateSwap = () => {
const fromAmount = parseFloat(swapFromAmountInput.value) || 0;
// Calculate exchange rate
const fromPrice = tokenData[fromToken].price;
const toPrice = tokenData[toToken].price;
const rate = fromPrice / toPrice;
// Calculate to amount
const toAmount = fromAmount * rate;
// Update UI
swapToAmountElement.value = toAmount.toFixed(2);
if (exchangeRateElement) {
exchangeRateElement.textContent = `1 ${fromToken} = ${rate.toFixed(4)} ${toToken}`;
}
if (minimumReceivedElement) {
// 0.5% slippage for demonstration
const minReceived = toAmount * 0.995;
minimumReceivedElement.textContent = `${minReceived.toFixed(2)} ${toToken}`;
}
if (priceImpactElement) {
// Mock price impact calculation
const impact = fromAmount > 1000 ? '0.5%' : '< 0.1%';
priceImpactElement.textContent = impact;
priceImpactElement.className = fromAmount > 1000 ? 'text-warning' : 'text-success';
}
};
// Initialize from token dropdown items
const fromTokenItems = document.querySelectorAll('[aria-labelledby="fromTokenDropdown"] .dropdown-item');
fromTokenItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
fromToken = this.dataset.token;
fromTokenSymbolElement.textContent = fromToken;
fromTokenImgElement.src = this.dataset.img;
fromTokenBalanceElement.textContent = tokenData[fromToken].balance;
calculateSwap();
});
});
// Initialize to token dropdown items
const toTokenItems = document.querySelectorAll('[aria-labelledby="toTokenDropdown"] .dropdown-item');
toTokenItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
toToken = this.dataset.token;
toTokenSymbolElement.textContent = toToken;
toTokenImgElement.src = this.dataset.img;
toTokenBalanceElement.textContent = tokenData[toToken].balance;
calculateSwap();
});
});
// Swap direction button
if (swapDirectionButton) {
swapDirectionButton.addEventListener('click', function() {
// Swap tokens
const tempToken = fromToken;
fromToken = toToken;
toToken = tempToken;
// Update UI
fromTokenSymbolElement.textContent = fromToken;
toTokenSymbolElement.textContent = toToken;
const tempImg = fromTokenImgElement.src;
fromTokenImgElement.src = toTokenImgElement.src;
toTokenImgElement.src = tempImg;
fromTokenBalanceElement.textContent = tokenData[fromToken].balance;
toTokenBalanceElement.textContent = tokenData[toToken].balance;
// Swap amounts
const tempAmount = swapFromAmountInput.value;
swapFromAmountInput.value = swapToAmountElement.value;
calculateSwap();
});
}
// Max button
if (maxFromButton) {
maxFromButton.addEventListener('click', function() {
// Set max amount based on token balance
const balance = parseInt(tokenData[fromToken].balance.split(' ')[0].replace(/,/g, ''));
swapFromAmountInput.value = balance;
calculateSwap();
});
}
swapFromAmountInput.addEventListener('input', calculateSwap);
// Initial calculation
calculateSwap();
}
// =============== COLLATERAL TAB ===============
// Collateral form calculations
const collateralAssetSelect = document.getElementById('collateralAsset');
const collateralAmountInput = document.getElementById('collateralAmount');
const collateralValueElement = document.getElementById('collateralValue');
const collateralUnitElement = document.getElementById('collateralUnit');
const collateralAvailableElement = document.getElementById('collateralAvailable');
const collateralAvailableUSDElement = document.getElementById('collateralAvailableUSD');
const collateralPurposeSelect = document.getElementById('collateralPurpose');
const loanTermGroup = document.getElementById('loanTermGroup');
const loanAmountGroup = document.getElementById('loanAmountGroup');
const syntheticAssetGroup = document.getElementById('syntheticAssetGroup');
const syntheticAmountGroup = document.getElementById('syntheticAmountGroup');
const loanAmountInput = document.getElementById('loanAmount');
const maxLoanAmountElement = document.getElementById('maxLoanAmount');
const syntheticAmountInput = document.getElementById('syntheticAmount');
const maxSyntheticAmountElement = document.getElementById('maxSyntheticAmount');
const collateralRatioElement = document.getElementById('collateralRatio');
const liquidationPriceElement = document.getElementById('liquidationPrice');
const liquidationUnitElement = document.getElementById('liquidationUnit');
if (collateralAssetSelect && collateralAmountInput) {
const calculateCollateralDetails = () => {
if (collateralAssetSelect.selectedIndex === 0) return;
const selectedOption = collateralAssetSelect.options[collateralAssetSelect.selectedIndex];
const assetType = selectedOption.dataset.type;
const assetValue = parseFloat(selectedOption.dataset.value) || 0;
const assetAmount = parseFloat(selectedOption.dataset.amount) || 0;
const assetUnit = selectedOption.dataset.unit || '';
// Update UI with asset details
if (collateralUnitElement) collateralUnitElement.textContent = assetUnit;
if (collateralAvailableElement) collateralAvailableElement.textContent = assetAmount.toLocaleString() + ' ' + assetUnit;
if (collateralAvailableUSDElement) collateralAvailableUSDElement.textContent = '$' + assetValue.toLocaleString();
if (liquidationUnitElement) liquidationUnitElement.textContent = assetUnit;
// Calculate collateral value
} else {
liquidationPriceElement.value = (borrowedValue * 1.2).toFixed(2);
}
if (collateralValueElement) collateralValueElement.value = collateralValue.toFixed(2);
// Calculate max loan amount (75% of collateral value)
const maxLoanAmount = collateralValue * 0.75;
if (maxLoanAmountElement) maxLoanAmountElement.textContent = maxLoanAmount.toFixed(2);
// Calculate max synthetic amount (50% of collateral value)
const maxSyntheticAmount = collateralValue * 0.5;
if (maxSyntheticAmountElement) maxSyntheticAmountElement.textContent = maxSyntheticAmount.toFixed(2);
// Calculate collateral ratio and liquidation price
updateCollateralRatio();
};
const updateCollateralRatio = () => {
const collateralValue = parseFloat(collateralValueElement.value) || 0;
const purpose = collateralPurposeSelect.value;
let borrowedValue = 0;
if (purpose === 'loan') {
borrowedValue = parseFloat(loanAmountInput.value) || 0;
} else if (purpose === 'synthetic') {
borrowedValue = parseFloat(syntheticAmountInput.value) || 0;
} else {
// For leverage trading, assume 2x leverage
borrowedValue = collateralValue;
}
// Calculate ratio
const ratio = borrowedValue > 0 ? (collateralValue / borrowedValue * 100) : 0;
if (collateralRatioElement) {
collateralRatioElement.value = ratio.toFixed(0) + '%';
}
// Calculate liquidation price
if (liquidationPriceElement) {
const selectedOption = collateralAssetSelect.options[collateralAssetSelect.selectedIndex];
if (selectedOption.selectedIndex === 0) return;
const assetType = selectedOption.dataset.type;
const assetValue = parseFloat(selectedOption.dataset.value) || 0;
const assetAmount = parseFloat(selectedOption.dataset.amount) || 0;
const collateralAmount = parseFloat(collateralAmountInput.value) || 0;
if (assetType === 'token' && collateralAmount > 0) {
const currentPrice = assetValue / assetAmount;
const liquidationThreshold = purpose === 'loan' ? 1.2 : 1.5; // 120% for loans, 150% for synthetic
const liquidationPrice = (borrowedValue / collateralAmount) * liquidationThreshold;
liquidationPriceElement.value = liquidationPrice.toFixed(4);
} else {
liquidationPriceElement.value = (borrowedValue * 1.2).toFixed(2);
}
}
};
// Handle collateral asset selection
collateralAssetSelect.addEventListener('change', function() {
collateralAmountInput.value = '';
calculateCollateralDetails();
});
// Handle collateral amount input
collateralAmountInput.addEventListener('input', calculateCollateralDetails);
// Handle purpose selection
collateralPurposeSelect.addEventListener('change', function() {
const purpose = collateralPurposeSelect.value;
// Show/hide relevant form groups
if (loanTermGroup) loanTermGroup.style.display = purpose === 'loan' ? 'block' : 'none';
if (loanAmountGroup) loanAmountGroup.style.display = purpose === 'loan' ? 'block' : 'none';
if (syntheticAssetGroup) syntheticAssetGroup.style.display = purpose === 'synthetic' ? 'block' : 'none';
if (syntheticAmountGroup) syntheticAmountGroup.style.display = purpose === 'synthetic' ? 'block' : 'none';
updateCollateralRatio();
});
// Handle loan amount input
if (loanAmountInput) {
loanAmountInput.addEventListener('input', updateCollateralRatio);
// Max loan button
const maxLoanButton = document.getElementById('maxLoanButton');
if (maxLoanButton) {
maxLoanButton.addEventListener('click', function() {
const maxLoan = parseFloat(maxLoanAmountElement.textContent) || 0;
loanAmountInput.value = maxLoan.toFixed(2);
updateCollateralRatio();
});
}
}
// Handle synthetic amount input
if (syntheticAmountInput) {
syntheticAmountInput.addEventListener('input', updateCollateralRatio);
// Max synthetic button
const maxSyntheticButton = document.getElementById('maxSyntheticButton');
if (maxSyntheticButton) {
maxSyntheticButton.addEventListener('click', function() {
const maxSynthetic = parseFloat(maxSyntheticAmountElement.textContent) || 0;
syntheticAmountInput.value = maxSynthetic.toFixed(2);
updateCollateralRatio();
});
}
}
// Handle synthetic asset selection
const syntheticAssetSelect = document.getElementById('syntheticAsset');
const syntheticUnitElement = document.getElementById('syntheticUnit');
const maxSyntheticUnitElement = document.getElementById('maxSyntheticUnit');
if (syntheticAssetSelect && syntheticUnitElement && maxSyntheticUnitElement) {
syntheticAssetSelect.addEventListener('change', function() {
const asset = syntheticAssetSelect.value;
syntheticUnitElement.textContent = asset;
maxSyntheticUnitElement.textContent = asset;
});
}
}
// Initialize tab functionality if not already handled by Bootstrap
const tabLinks = document.querySelectorAll('.nav-link[data-bs-toggle="tab"]');
tabLinks.forEach(tabLink => {
tabLink.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetTab = document.querySelector(targetId);
// Hide all tabs
document.querySelectorAll('.tab-pane').forEach(tab => {
tab.classList.remove('show', 'active');
});
// Show the target tab
if (targetTab) {
targetTab.classList.add('show', 'active');
}
// Update active state on nav links
tabLinks.forEach(link => link.classList.remove('active'));
this.classList.add('active');
});
});
});

2809
flowbroker/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
flowbroker/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "flowbroker"
version = "0.1.0"
edition = "2024"
[dependencies]
sigsocket = { path = "../sigsocket" } # Path relative to flowbroker directory
actix-web = "4.3.1"
actix-rt = "2.8.0"
actix-files = "0.6.2"
actix-web-actors = "4.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
env_logger = "0.10.0"
log = "0.4.0"
tera = "1.19.0"
tokio = { version = "1.28.0", features = ["full"] }
dotenv = "0.15.0"
hex = "0.4.3"
uuid = { version = "1.4", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] } # For timestamps
rhai = "1.18.0"
serde_urlencoded = "0.7"
# Database models and ORM-like functionality
heromodels = { path = "../../db/heromodels" }
# Note: heromodels pulls in 'ourdb', 'heromodels_core', 'heromodels_derive'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
148

Binary file not shown.

690
flowbroker/src/main.rs Normal file
View File

@@ -0,0 +1,690 @@
use actix_files as fs;
use actix_web::{web, App, HttpResponse, HttpServer, Responder, Result as ActixResult};
use std::fs as std_fs;
use std::path::PathBuf;
use actix_web_actors::ws;
use serde::{Deserialize, Serialize};
use serde_urlencoded; // Added for from_str
use tera::{Tera, Context};
use std::sync::{Arc, Mutex, RwLock};
use sigsocket::service::SigSocketService;
use sigsocket::registry::ConnectionRegistry;
use log::{info, error};
use uuid::Uuid;
use rhai::{Engine, EvalAltResult, Position};
// use std::collections::HashMap; // Removed as no longer used
use heromodels; // Added for database models
use heromodels::db::hero::OurDB;
use heromodels::db::{Db, Collection}; // Import Db trait for .collection() and Collection trait for .set()/.get_all()
use heromodels::models::flowbroker_models::{Flow, FlowStep, SignatureRequirement}; // Import the models
use dotenv::dotenv;
use std::env;
// --- Flowbroker Specific Enums (to be used by application logic) ---
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum FlowStepStatus {
Pending, // Step created, not yet processed
InProgress, // Step is actively being processed (e.g., waiting for signatures)
Completed, // All requirements for this step are met
Failed, // Step failed (e.g., a signature requirement failed or timed out)
Skipped, // Step was skipped (e.g., due to conditional logic not yet implemented)
}
impl FlowStepStatus {
pub fn to_db_string(&self) -> String {
format!("{:?}", self)
}
pub fn from_db_string(s: &str) -> Result<Self, String> {
match s {
"Pending" => Ok(FlowStepStatus::Pending),
"InProgress" => Ok(FlowStepStatus::InProgress),
"Completed" => Ok(FlowStepStatus::Completed),
"Failed" => Ok(FlowStepStatus::Failed),
"Skipped" => Ok(FlowStepStatus::Skipped),
_ => Err(format!("Invalid FlowStepStatus string: {}", s)),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum SignatureRequirementStatus {
Pending, // Not yet processed or sent for signing
SentToClient, // Sent to client via SigSocket, awaiting signature
Signed, // Successfully signed
Failed, // Signing failed (e.g., client rejected, timeout, error)
Error, // An internal error occurred processing this requirement
}
impl SignatureRequirementStatus {
pub fn to_db_string(&self) -> String {
format!("{:?}", self)
}
pub fn from_db_string(s: &str) -> Result<Self, String> {
match s {
"Pending" => Ok(SignatureRequirementStatus::Pending),
"SentToClient" => Ok(SignatureRequirementStatus::SentToClient),
"Signed" => Ok(SignatureRequirementStatus::Signed),
"Failed" => Ok(SignatureRequirementStatus::Failed),
"Error" => Ok(SignatureRequirementStatus::Error),
_ => Err(format!("Invalid SignatureRequirementStatus string: {}", s)),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum FlowStatus {
Pending, // Flow created, no steps initiated
InProgress, // Flow started, steps are being processed
Completed, // All steps successfully signed
Failed, // A step failed or timed out
}
impl FlowStatus {
pub fn to_db_string(&self) -> String {
format!("{:?}", self)
}
pub fn from_db_string(s: &str) -> Result<Self, String> {
match s {
"Pending" => Ok(FlowStatus::Pending),
"InProgress" => Ok(FlowStatus::InProgress),
"Completed" => Ok(FlowStatus::Completed),
"Failed" => Ok(FlowStatus::Failed),
_ => Err(format!("Invalid FlowStatus string: {}", s)),
}
}
}
// NOTE: The old Flow, FlowStep, and SignatureRequirement structs previously here
// have been removed. Their definitions are now in the heromodels crate.
// --- AppState ---
pub struct AppState {
templates: Tera,
sigsocket_service: Arc<SigSocketService>,
db: Arc<OurDB>, // Using OurDB from heromodels
next_id_counter: Arc<Mutex<u32>>, // For generating temporary primary keys
}
// --- Form Deserialization (for new dynamic form) ---
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct RequirementRealFormData {
// The name attributes in HTML are like: steps[0][requirements][0][message]
pub message: String, // Made fields public for external construction in tests
pub public_key: String, // Made fields public
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct FlowStepFormData {
description: Option<String>, // If description field is optional and might not be present
requirements: Vec<RequirementRealFormData>,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct CreateFlowRealFormData { // Renamed to avoid confusion with heromodels::Flow
flow_name: String,
steps: Vec<FlowStepFormData>,
}
#[derive(serde::Deserialize, Debug)]
pub struct RhaiScriptFormData {
rhai_script: String,
}
// --- Context Structs for Templates ---
#[derive(Serialize, Clone)]
struct RhaiExampleDisplay {
name: String,
content: String,
}
#[derive(Serialize)]
struct ListFlowsContext {
flows: Vec<Flow>, // Using heromodels::models::flowbroker_models::Flow
example_scripts: Vec<RhaiExampleDisplay>,
error_message: Option<String>,
success_message: Option<String>,
}
// --- Handlers ---
// Display list of flows
async fn list_flows(data: web::Data<AppState>) -> ActixResult<HttpResponse> {
let tera = &data.templates;
// Fetch actual flows from the database
let flows_collection = data.db.collection::<Flow>()
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("DB Error: Failed to get flows collection: {}", e)))?;
let (mut flows, flow_error_message) = match flows_collection.get_all() {
Ok(mut flows_vec) => {
flows_vec.sort_by(|a, b| b.base_data.created_at.cmp(&a.base_data.created_at)); // Sort by newest
(flows_vec, None)
},
Err(e) => {
error!("Failed to fetch flows: {:?}", e);
(Vec::new(), Some(format!("Error fetching flows: {:?}", e)))
}
};
// Load Rhai example scripts
let examples_path = PathBuf::from("templates/rhai_examples");
let mut example_scripts_display = Vec::new();
if examples_path.is_dir() {
match std_fs::read_dir(examples_path) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && path.extension().and_then(std::ffi::OsStr::to_str) == Some("rhai") {
let file_stem = path.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Unknown Script");
// Convert filename (e.g., simple_two_step) to a nicer name (e.g., Simple Two Step)
let script_name = file_stem.replace("_", " ")
.split_whitespace()
.map(|word| {
let mut c = word.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect::<Vec<String>>().join(" ");
match std_fs::read_to_string(&path) {
Ok(content) => {
example_scripts_display.push(RhaiExampleDisplay { name: script_name, content });
}
Err(e) => {
error!("Failed to read Rhai example script {:?}: {}", path, e);
}
}
}
}
}
}
Err(e) => {
error!("Failed to read Rhai examples directory: {}", e);
}
}
}
example_scripts_display.sort_by(|a, b| a.name.cmp(&b.name));
let list_context = ListFlowsContext {
flows,
example_scripts: example_scripts_display,
error_message: flow_error_message,
success_message: None, // TODO: Populate from query params or session later if needed
};
let tera_ctx = Context::from_serialize(&list_context).unwrap_or_else(|e| {
error!("Failed to serialize ListFlowsContext: {}", e);
// Fallback to a minimal context or an error state if serialization fails
let mut err_ctx = Context::new();
err_ctx.insert("error_message", &"Critical error preparing page data.".to_string());
err_ctx
});
// Still rendering to index.html, which will be the revamped list_flows.html
let rendered = tera.render("index.html", &tera_ctx)
.map_err(|e| actix_web::error::ErrorInternalServerError(format!("Template error (index.html): {}", e)))?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
// Handle creation of a new flow
async fn create_flow(
data: web::Data<AppState>,
raw_form_data: String, // Changed to accept raw String
) -> impl Responder {
info!("Received raw form data for create_flow: {}", raw_form_data);
// Attempt to parse the raw form data
let form_parse_result: Result<CreateFlowRealFormData, serde_urlencoded::de::Error> = serde_urlencoded::from_str(&raw_form_data);
let form = match form_parse_result {
Ok(parsed_form_data) => {
info!("Successfully parsed form data: {:?}", parsed_form_data);
parsed_form_data // Use the successfully parsed data
}
Err(e) => {
error!("Failed to parse form data from string: {}. Raw data: {}", e, raw_form_data);
return HttpResponse::BadRequest().body(format!("Form parsing error: {}. Please check input and logs.", e));
}
};
// --- Logic starts here, using `form` which is now CreateFlowRealFormData ---
info!("Processing create_flow request for: {}", form.flow_name);
let db = &data.db;
let mut id_counter = match data.next_id_counter.lock() {
Ok(guard) => guard,
Err(poisoned) => {
error!("Mutex for next_id_counter was poisoned: {}. Recovering.", poisoned);
poisoned.into_inner() // Attempt to recover
}
};
// 1. Create and save the main Flow object
*id_counter += 1;
let flow_db_id = *id_counter;
let flow_uuid = Uuid::new_v4().to_string();
let flow_instance = Flow::new(
flow_db_id,
&flow_uuid,
&form.flow_name,
FlowStatus::Pending.to_db_string() // Use local enum's string representation
);
match db.collection::<Flow>() {
Ok(flow_collection) => {
if let Err(e) = flow_collection.set(&flow_instance) {
error!("Failed to save Flow (name: {}): {:?}. Aborting flow creation.", form.flow_name, e);
return HttpResponse::InternalServerError().body(format!("Failed to save main flow data: {:?}", e));
}
info!("Saved Flow object for '{}', UUID: {}, DB_ID: {}", flow_instance.name, flow_instance.flow_uuid, flow_instance.base_data.id);
}
Err(e) => {
error!("Failed to get Flow collection: {:?}. Aborting flow creation.", e);
return HttpResponse::InternalServerError().body(format!("Database error getting flow collection: {:?}", e));
}
}
// 2. Create and save FlowStep and SignatureRequirement objects
for (step_idx, step_form_data) in form.steps.into_iter().enumerate() {
*id_counter += 1;
let flow_step_db_id = *id_counter;
let mut flow_step_instance = FlowStep::new(
flow_step_db_id,
flow_instance.base_data.id, // Use ID from the saved Flow instance
step_idx as u32, // step_order
FlowStepStatus::Pending.to_db_string() // Use local enum's string representation
);
if let Some(desc) = step_form_data.description {
if !desc.is_empty() { // Only set if description is not empty
flow_step_instance = flow_step_instance.description(desc);
}
}
match db.collection::<FlowStep>() {
Ok(step_collection) => {
if let Err(e) = step_collection.set(&flow_step_instance) {
error!("Failed to save FlowStep (flow: {}, step_idx: {}): {:?}", flow_instance.name, step_idx, e);
return HttpResponse::InternalServerError().body(format!("Failed to save flow step: {:?}", e));
}
info!("Saved FlowStep {} for flow '{}', DB_ID: {}", step_idx + 1, flow_instance.name, flow_step_instance.base_data.id);
}
Err(e) => {
error!("Failed to get FlowStep collection: {:?}. Aborting.", e);
return HttpResponse::InternalServerError().body(format!("Database error getting step collection: {:?}", e));
}
}
for (req_idx, req_form_data) in step_form_data.requirements.into_iter().enumerate() {
*id_counter += 1;
let sig_req_db_id = *id_counter;
let sig_req_instance = SignatureRequirement::new(
sig_req_db_id,
flow_step_instance.base_data.id, // Use ID from the saved FlowStep instance
&req_form_data.public_key,
&req_form_data.message,
SignatureRequirementStatus::Pending.to_db_string() // Use local enum's string representation
);
match db.collection::<SignatureRequirement>() {
Ok(req_collection) => {
if let Err(e) = req_collection.set(&sig_req_instance) {
error!("Failed to save SignatureRequirement (flow: {}, step: {}, req_idx: {}): {:?}", flow_instance.name, step_idx, req_idx, e);
return HttpResponse::InternalServerError().body(format!("Failed to save signature requirement: {:?}", e));
}
info!(
"Saved SignatureRequirement {} for step {} of flow '{}', DB_ID: {}",
req_idx + 1, step_idx + 1, flow_instance.name, sig_req_instance.base_data.id
);
}
Err(e) => {
error!("Failed to get SignatureRequirement collection: {:?}. Aborting.", e);
return HttpResponse::InternalServerError().body(format!("Database error getting requirement collection: {:?}", e));
}
}
}
}
info!("Finished processing all steps for flow '{}', UUID: {}", flow_instance.name, flow_instance.flow_uuid);
HttpResponse::SeeOther()
.append_header((actix_web::http::header::LOCATION, "/"))
.finish()
}
// --- Rhai-Callable Helper Functions ---
fn rhai_create_flow_entry(
db_arc: Arc<OurDB>,
id_counter_arc: Arc<Mutex<u32>>,
name: String,
) -> Result<u32, Box<rhai::EvalAltResult>> {
info!("Rhai: Attempting to create flow entry with name: {}", name);
let mut id_counter = match id_counter_arc.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
};
*id_counter += 1;
let flow_db_id = *id_counter;
let flow_uuid = Uuid::new_v4().to_string();
let flow_instance = Flow::new(
flow_db_id,
&flow_uuid,
&name,
FlowStatus::Pending.to_db_string(),
);
match db_arc.collection::<Flow>() {
Ok(flow_collection) => {
if let Err(e) = flow_collection.set(&flow_instance) {
let err_msg = format!("Rhai: Failed to save Flow (name: {}): {:?}", name, e);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
info!("Rhai: Saved Flow object for '{}', UUID: {}, DB_ID: {}", flow_instance.name, flow_instance.flow_uuid, flow_instance.base_data.id);
Ok(flow_instance.base_data.id)
}
Err(e) => {
let err_msg = format!("Rhai: Failed to get Flow collection: {:?}", e);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
}
}
fn rhai_add_step_entry(
db_arc: Arc<OurDB>,
id_counter_arc: Arc<Mutex<u32>>,
flow_db_id: u32, // ID of the parent flow
description: String,
order: u32,
) -> Result<u32, Box<rhai::EvalAltResult>> {
info!(
"Rhai: Adding step to flow ID {}, order {}, description: '{}'",
flow_db_id, order, description
);
let mut id_counter = match id_counter_arc.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
};
*id_counter += 1;
let flow_step_db_id = *id_counter;
let mut flow_step_instance = FlowStep::new(
flow_step_db_id,
flow_db_id,
order,
FlowStepStatus::Pending.to_db_string(),
);
if !description.is_empty() {
flow_step_instance = flow_step_instance.description(description);
}
match db_arc.collection::<FlowStep>() {
Ok(step_collection) => {
if let Err(e) = step_collection.set(&flow_step_instance) {
let err_msg = format!(
"Rhai: Failed to save FlowStep (flow_id: {}, order: {}): {:?}",
flow_db_id, order, e
);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
info!(
"Rhai: Saved FlowStep for flow_id {}, order {}, DB_ID: {}",
flow_db_id, order, flow_step_instance.base_data.id
);
Ok(flow_step_instance.base_data.id)
}
Err(e) => {
let err_msg = format!("Rhai: Failed to get FlowStep collection: {:?}", e);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
}
}
fn rhai_add_requirement_entry(
db_arc: Arc<OurDB>,
id_counter_arc: Arc<Mutex<u32>>,
step_db_id: u32, // ID of the parent step
public_key: String,
message: String,
) -> Result<u32, Box<rhai::EvalAltResult>> {
info!(
"Rhai: Adding requirement to step ID {}, pk: '{}', msg: '{}'",
step_db_id, public_key, message
);
let mut id_counter = match id_counter_arc.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let err_msg = format!("Rhai: Mutex for next_id_counter was poisoned: {}", poisoned);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
};
*id_counter += 1;
let sig_req_db_id = *id_counter;
let sig_req_instance = SignatureRequirement::new(
sig_req_db_id,
step_db_id,
&public_key,
&message,
SignatureRequirementStatus::Pending.to_db_string(),
);
match db_arc.collection::<SignatureRequirement>() {
Ok(req_collection) => {
if let Err(e) = req_collection.set(&sig_req_instance) {
let err_msg = format!(
"Rhai: Failed to save SigRequirement (step_id: {}): {:?}",
step_db_id, e
);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
info!(
"Rhai: Saved SigRequirement for step_id {}, DB_ID: {}",
step_db_id, sig_req_instance.base_data.id
);
Ok(sig_req_instance.base_data.id)
}
Err(e) => {
let err_msg = format!("Rhai: Failed to get SigRequirement collection: {:?}", e);
error!("{}", err_msg);
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(err_msg.into(), Position::NONE)));
}
}
}
// Handle creation of a new flow from a Rhai script
async fn create_flow_from_script(
data: web::Data<AppState>,
form: web::Form<RhaiScriptFormData>,
) -> impl Responder {
info!("Received Rhai script for flow creation:\n{}", form.rhai_script);
let mut engine = Engine::new();
// Clone Arcs for capturing in closures
let db_clone_for_flow = data.db.clone();
let id_clone_for_flow = data.next_id_counter.clone();
let db_clone_for_step = data.db.clone();
let id_clone_for_step = data.next_id_counter.clone();
let db_clone_for_req = data.db.clone();
let id_clone_for_req = data.next_id_counter.clone();
engine
.register_fn("create_flow", move |name: String| {
crate::rhai_create_flow_entry(db_clone_for_flow.clone(), id_clone_for_flow.clone(), name)
})
.register_fn("add_step", move |flow_id: u32, desc: String, order: i64| {
if order < 0 || order > u32::MAX as i64 {
return Err(Box::new(EvalAltResult::ErrorRuntime(format!("Order {} is out of range for u32", order).into(), Position::NONE)));
}
crate::rhai_add_step_entry(db_clone_for_step.clone(), id_clone_for_step.clone(), flow_id, desc, order as u32)
})
.register_fn("add_requirement", move |step_id: u32, pk: String, msg: String| {
crate::rhai_add_requirement_entry(db_clone_for_req.clone(), id_clone_for_req.clone(), step_id, pk, msg)
});
match engine.eval::<()>(&form.rhai_script) { // Expecting () as successful script execution doesn't need to return a value to Rust here.
Ok(_) => {
info!("Rhai script executed successfully.");
HttpResponse::SeeOther()
.append_header((actix_web::http::header::LOCATION, "/"))
.finish()
}
Err(e) => {
error!("Rhai script execution failed: {}", e.to_string());
HttpResponse::BadRequest().body(format!("Rhai script error: {}\n\nYour script was:\n{}", e.to_string(), form.rhai_script))
}
}
}
// Placeholder for SigSocket WebSocket handler
async fn websocket_handler(
req: actix_web::HttpRequest,
stream: actix_web::web::Payload,
service: web::Data<Arc<SigSocketService>>,
) -> ActixResult<HttpResponse> {
info!("WebSocket connection attempt");
let handler = service.create_websocket_handler();
ws::start(handler, &req, stream)
}
// --- Extracted Helper Functions for App Setup and Configuration ---
/// Sets up the shared application data (AppState).
/// Allows overriding the database path for testing purposes.
pub async fn setup_app_data(db_path_override: Option<String>) -> Result<web::Data<AppState>, std::io::Error> {
// Initialize templates
let tera = match Tera::new("templates/**/*") {
Ok(t) => t,
Err(e) => {
error!("Critical: Tera template parsing error(s): {}", e);
// Convert tera::Error to std::io::Error
return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Tera init error: {}", e)));
}
};
// Initialize SigSocket registry and service
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
// Load environment variables from .env file
dotenv().ok();
// Initialize Database
let database_path = db_path_override.unwrap_or_else(||
env::var("DATABASE_PATH").unwrap_or_else(|_|
{
info!("DATABASE_PATH not set, defaulting to ./flowbroker_db");
"./flowbroker_db".to_string()
})
);
let db = match OurDB::new(&database_path, true) { // true for create_if_missing
Ok(db_instance) => Arc::new(db_instance),
Err(e) => {
error!("Failed to initialize database at '{}': {}. Please ensure the path is writable.", database_path, e);
// Convert heromodels::Error to std::io::Error (assuming Error impls std::error::Error)
return Err(std::io::Error::new(std::io::ErrorKind::Other, format!("DB init error: {}", e)));
}
};
info!("Database initialized at: {}", database_path);
// Initialize ID counter for temporary primary keys
let next_id_counter = Arc::new(Mutex::new(0_u32));
// TODO: Replace this with a robust primary key generation strategy from the database itself if possible.
// Create shared application state
Ok(web::Data::new(AppState {
templates: tera,
sigsocket_service: sigsocket_service.clone(), // Clone for AppState
db,
next_id_counter,
}))
}
/// Configures the application routes.
pub fn configure_app_routes(cfg: &mut web::ServiceConfig) {
// Note: AppState should be added via .app_data() before calling this configure function.
// The websocket_handler specifically needs web::Data<Arc<SigSocketService>>.
// The main HttpServer setup will add AppState (which includes an Arc<SigSocketService>)
// and also the specific web::Data<Arc<SigSocketService>> for handlers like websocket_handler that expect it directly.
cfg.route("/", web::get().to(list_flows))
.service(
web::scope("/flows") // Group flow-related routes under /flows
// .route("", web::get().to(list_flows)) // If you want /flows to also list flows
// .route("/new", web::get().to(new_flow_form)) // Deprecated, functionality merged into root list_flows
.route("/create", web::post().to(create_flow))
.route("/create_script", web::post().to(create_flow_from_script)) // Moved inside /flows scope
)
.service(web::resource("/ws/").route(web::get().to(websocket_handler)))
.service(fs::Files::new("/static", "./static").show_files_listing()); // Static files
}
// --- Main Function ---
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
let app_data = match setup_app_data(None).await {
Ok(data) => data,
Err(e) => {
error!("Failed to setup application data: {}", e);
std::process::exit(1);
}
};
// The AppState (app_data) already contains an Arc<SigSocketService>.
// Handlers like websocket_handler that take web::Data<Arc<SigSocketService>> directly
// will be able to access it if AppState is correctly registered and the handler signature matches.
// Alternatively, if a handler needs *only* the SigSocketService, it can be added separately.
// For the websocket_handler as defined (taking web::Data<Arc<SigSocketService>>),
// it needs this specific type registered with app_data.
let sigsocket_service_for_ws_handler_data = web::Data::new(app_data.sigsocket_service.clone());
info!("Flowbroker server starting on http://127.0.0.1:8081");
info!("SigSocket WebSocket endpoint available at ws://127.0.0.1:8081/ws");
HttpServer::new(move || {
App::new()
.app_data(app_data.clone()) // Main app state (includes SigSocketService)
.app_data(sigsocket_service_for_ws_handler_data.clone()) // Specifically for handlers expecting web::Data<Arc<SigSocketService>>
.configure(configure_app_routes)
})
.bind("127.0.0.1:8081")? // Using a different port for now
.run()
.await
}

34
flowbroker/start.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/zsh
FORCE_KILL=false
# Parse command line options
while getopts ":f" opt; do
case ${opt} in
f )
FORCE_KILL=true
;;
\? )
echo "Usage: cmd [-f]"
exit 1
;;
esac
done
if [ "$FORCE_KILL" = true ] ; then
echo "Attempting to kill process on port 8081..."
# Get PID of process using port 8081 and kill it
# -t option for lsof outputs only the PID
# xargs -r ensures kill is only run if lsof finds a PID
lsof -t -i:8081 | xargs -r kill -9
if [ $? -eq 0 ]; then
echo "Process(es) on port 8081 killed."
else
echo "No process found on port 8081 or failed to kill."
fi
# Give a moment for the port to be released
sleep 1
fi
echo "Starting Flowbroker server..."
cargo run

127
flowbroker/static/style.css Normal file
View File

@@ -0,0 +1,127 @@
body {
font-family: sans-serif;
margin: 20px;
line-height: 1.6;
}
h1, h2 {
color: #333;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
form div {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], textarea {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
hr {
margin: 20px 0;
}
#flows-list ul {
list-style-type: none;
padding: 0;
}
#flows-list li {
border: 1px solid #eee;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
/* Styles for dynamic form elements from create_flow.html */
.step, .requirement {
border: 1px solid #ddd;
padding: 15px; /* Increased padding */
margin-bottom: 15px;
border-radius: 4px;
background-color: #f9f9f9;
}
.step h3, .step h4, .requirement h5 {
margin-top: 0;
color: #555; /* Slightly softer color */
}
.step .requirementsContainer {
margin-left: 20px;
border-left: 3px solid #007bff; /* Thicker border */
padding-left: 20px; /* Increased padding */
margin-top: 10px;
margin-bottom: 10px;
}
button.removeStepBtn, button.removeRequirementBtn {
background-color: #dc3545;
color: white;
padding: 5px 10px; /* Adjusted padding */
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px; /* Increased margin */
float: right; /* Align to the right */
}
button.removeStepBtn:hover, button.removeRequirementBtn:hover {
background-color: #c82333;
}
/* Clearfix for floated remove buttons */
.step::after, .requirement::after {
content: "";
clear: both;
display: table;
}
.addBtn { /* Style for Add Step / Add Requirement buttons */
background-color: #28a745;
color: white;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
margin-bottom: 10px;
}
.addBtn:hover {
background-color: #218838;
}
/* General styling for form elements within steps/requirements for consistency */
.step input[type="text"], .step textarea,
.requirement input[type="text"], .requirement textarea {
margin-bottom: 8px; /* Add some space below inputs */
}

View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Flowbroker - Create Flow</title>
<link rel="stylesheet" href="/static/style.css">
<style>
.step, .requirement {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
background-color: #f9f9f9;
}
.step h3, .step h4, .requirement h5 {
margin-top: 0;
}
.step .requirementsContainer {
margin-left: 20px;
border-left: 2px solid #007bff;
padding-left: 15px;
}
button.removeStepBtn, button.removeRequirementBtn {
background-color: #dc3545;
margin-top: 5px;
}
button.removeStepBtn:hover, button.removeRequirementBtn:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<h1>Create New Flow</h1>
<form id="createFlowForm" action="/flows" method="post">
<div>
<label for="flow_name">Flow Name:</label>
<input type="text" id="flow_name" name="flow_name" required>
</div>
<hr>
<div id="stepsContainer">
<!-- Steps will be added here by JavaScript -->
</div>
<button type="button" id="addStepBtn" class="addBtn">Add Step</button>
<hr>
<button type="submit">Create Flow</button>
</form>
<p><a href="/">Back to Flows List</a></p>
<!-- Template for a new step -->
<template id="stepTemplate">
<div class="step" data-step-index="">
<h3>Step <span class="step-number"></span></h3>
<button type="button" class="removeStepBtn">Remove This Step</button>
<div>
<label>Step Description (Optional):</label>
<input type="text" name="steps[X].description" class="step-description">
</div>
<h4>Signature Requirements for Step <span class="step-number"></span></h4>
<div class="requirementsContainer" data-step-index="">
<!-- Requirements will be added here -->
</div>
<button type="button" class="addRequirementBtn addBtn" data-step-index="">Add Signature Requirement</button>
</div>
</template>
<!-- Template for a new signature requirement -->
<template id="requirementTemplate">
<div class="requirement" data-req-index="">
<h5>Requirement <span class="req-number"></span></h5>
<button type="button" class="removeRequirementBtn">Remove Requirement</button>
<div>
<label>Message to Sign:</label>
<textarea name="steps[X].requirements[Y].message" rows="2" required class="req-message"></textarea>
</div>
<div>
<label>Required Public Key:</label>
<input type="text" name="steps[X].requirements[Y].public_key" required class="req-pubkey">
</div>
</div>
</template>
<script>
document.addEventListener('DOMContentLoaded', () => {
const stepsContainer = document.getElementById('stepsContainer');
const addStepBtn = document.getElementById('addStepBtn');
const stepTemplate = document.getElementById('stepTemplate');
const requirementTemplate = document.getElementById('requirementTemplate');
const form = document.getElementById('createFlowForm');
const updateIndices = () => {
const steps = stepsContainer.querySelectorAll('.step');
steps.forEach((step, stepIdx) => {
// Update step-level attributes and text
step.dataset.stepIndex = stepIdx;
step.querySelector('.step-number').textContent = stepIdx + 1;
step.querySelector('.step-description').name = `steps[${stepIdx}].description`;
const addReqBtn = step.querySelector('.addRequirementBtn');
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
const requirements = step.querySelectorAll('.requirementsContainer .requirement');
requirements.forEach((req, reqIdx) => {
// Update requirement-level attributes and text
req.dataset.reqIndex = reqIdx;
req.querySelector('.req-number').textContent = reqIdx + 1;
req.querySelector('.req-message').name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
req.querySelector('.req-pubkey').name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
});
});
};
const addRequirement = (currentStepElement, stepIndex) => {
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer');
const reqFragment = requirementTemplate.content.cloneNode(true);
const newRequirement = reqFragment.querySelector('.requirement');
requirementsContainer.appendChild(newRequirement);
updateIndices(); // Update all indices after adding
};
const addStep = () => {
const stepFragment = stepTemplate.content.cloneNode(true);
const newStep = stepFragment.querySelector('.step');
stepsContainer.appendChild(newStep);
// Add at least one requirement to the new step automatically
const currentStepIndex = stepsContainer.querySelectorAll('.step').length - 1;
addRequirement(newStep, currentStepIndex);
updateIndices(); // Update all indices after adding
};
// Event delegation for remove buttons and add requirement button
stepsContainer.addEventListener('click', (event) => {
if (event.target.classList.contains('removeStepBtn')) {
event.target.closest('.step').remove();
if (stepsContainer.querySelectorAll('.step').length === 0) { // Ensure at least one step
addStep();
}
updateIndices();
} else if (event.target.classList.contains('addRequirementBtn')) {
const stepElement = event.target.closest('.step');
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement(stepElement, stepIndex);
} else if (event.target.classList.contains('removeRequirementBtn')) {
const requirementElement = event.target.closest('.requirement');
const stepElement = event.target.closest('.step');
const requirementsContainer = stepElement.querySelector('.requirementsContainer');
requirementElement.remove();
// Ensure at least one requirement per step
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement(stepElement, stepIndex);
}
updateIndices();
}
});
addStepBtn.addEventListener('click', addStep);
// Add one step by default when the page loads
if (stepsContainer.children.length === 0) {
addStep();
}
// Optional: Validate that there's at least one step and one requirement before submit
form.addEventListener('submit', (event) => {
if (stepsContainer.querySelectorAll('.step').length === 0) {
alert('Please add at least one step to the flow.');
event.preventDefault();
return;
}
const steps = stepsContainer.querySelectorAll('.step');
for (let i = 0; i < steps.length; i++) {
if (steps[i].querySelectorAll('.requirementsContainer .requirement').length === 0) {
alert(`Step ${i + 1} must have at least one signature requirement.`);
event.preventDefault();
return;
}
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,317 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FlowBroker Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<!-- Optional: Link to your custom style.css if needed, but ensure it doesn't conflict heavily with Bootstrap -->
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">FlowBroker</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Dashboard</a>
</li>
<!-- Add other nav items here later if needed -->
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% if error_message %}
<div class="alert alert-danger" role="alert">
{{ error_message }}
</div>
{% endif %}
{% if success_message %}
<div class="alert alert-success" role="alert">
{{ success_message }}
</div>
{% endif %}
<h2>Active Flows</h2>
<div id="flows-list" class="mb-4">
{% if flows %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">UUID</th>
<th scope="col">Status</th>
<th scope="col">Created At</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for flow in flows %}
<tr>
<td>{{ flow.name }}</td>
<td><small>{{ flow.flow_uuid }}</small></td>
<td><span class="badge bg-secondary">{{ flow.status | default(value="Unknown") }}</span></td>
<td>{{ flow.base_data.created_at | date(format="%Y-%m-%d %H:%M:%S") }}</td>
<td>
<a href="/flows/{{ flow.flow_uuid }}" class="btn btn-sm btn-primary">View</a>
<button class="btn btn-sm btn-success run-flow-btn" data-flow-uuid="{{ flow.flow_uuid }}">Run</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No active flows found. You can create one below.</p>
{% endif %}
</div>
<hr class="my-4">
<h2>Runnable Example Scripts</h2>
<div id="example-scripts-list" class="mb-4">
{% if example_scripts %}
<ul class="list-group">
{% for example in example_scripts %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ example.name }}
<button class="btn btn-sm btn-info load-script-btn" data-script-content="{{ example.content | escape }}">Load into Form</button>
</li>
{% endfor %}
</ul>
{% else %}
<p>No example scripts found.</p>
{% endif %}
</div>
<hr class="my-4">
<div class="row">
<div class="col-md-6 mb-4">
<h2>Create New Flow from Rhai Script</h2>
<div id="create-from-rhai-section">
<form action="/flows/create_script" method="POST">
<div class="mb-3">
<label for="rhai_script_content" class="form-label">Rhai Script:</label>
<textarea class="form-control" id="rhai_script_content" name="rhai_script" rows="10" placeholder="Enter your Rhai script here or select an example using the 'Load into Form' buttons above..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Create Flow from Script</button>
</form>
</div>
</div>
<div class="col-md-6 mb-4">
<h2>Create New Flow (Step-by-Step UI)</h2>
<div id="create-step-by-step-section">
<form id="createFlowForm_dynamic" action="/flows/create" method="post">
<div class="mb-3">
<label for="flow_name_dynamic" class="form-label">Flow Name:</label>
<input type="text" id="flow_name_dynamic" name="flow_name" class="form-control" required>
</div>
<hr>
<div id="stepsContainer_dynamic" class="mb-3">
<!-- Steps will be added here by JavaScript -->
<p class="text-muted"><em>Steps will appear here as you add them.</em></p>
</div>
<button type="button" id="addStepBtn_dynamic" class="btn btn-secondary mb-3">Add Step</button>
<hr>
<button type="submit" class="btn btn-primary">Create Flow (Step-by-Step)</button>
</form>
</div>
</div>
</div>
</div>
<!-- Templates for Dynamic Step-by-Step Form -->
<template id="stepTemplate_dynamic">
<div class="step card mb-3" data-step-index="">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Step <span class="step-number"></span></h5>
<button type="button" class="btn btn-danger btn-sm removeStepBtn_dynamic">Remove This Step</button>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Step Description (Optional):</label>
<input type="text" name="steps[X].description" class="form-control step-description_dynamic">
</div>
<h6>Signature Requirements for Step <span class="step-number"></span>:</h6>
<div class="requirementsContainer_dynamic ps-3" data-step-index="">
<!-- Requirements will be added here by JS -->
<p class="text-muted small"><em>Requirements for this step will appear here.</em></p>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm addRequirementBtn_dynamic mt-2" data-step-index="">Add Signature Requirement</button>
</div>
</div>
</template>
<template id="requirementTemplate_dynamic">
<div class="requirement card mb-2" data-req-index="">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Requirement <span class="req-number"></span></strong>
<button type="button" class="btn btn-danger btn-sm removeRequirementBtn_dynamic">Remove Requirement</button>
</div>
<div class="mb-2">
<label class="form-label">Message to Sign:</label>
<textarea name="steps[X].requirements[Y].message" rows="2" class="form-control req-message_dynamic" required></textarea>
</div>
<div>
<label class="form-label">Required Public Key:</label>
<input type="text" name="steps[X].requirements[Y].public_key" class="form-control req-pubkey_dynamic" required>
</div>
</div>
</div>
</template>
<!-- End of Templates -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<script>
// Basic script to handle 'Load into Form' for example scripts
document.addEventListener('DOMContentLoaded', function () {
const loadScriptButtons = document.querySelectorAll('.load-script-btn');
loadScriptButtons.forEach(button => {
button.addEventListener('click', function () {
const scriptContent = this.dataset.scriptContent;
const rhaiTextarea = document.querySelector('#rhai_script_content'); // Assuming this ID for the textarea
if (rhaiTextarea) {
rhaiTextarea.value = scriptContent;
// Optionally, scroll to the form or give some visual feedback
rhaiTextarea.focus();
alert('Script loaded into the textarea below!');
} else {
alert('Rhai script textarea not found on the page. It will be added soon.');
}
});
});
// --- Start of Dynamic Step-by-Step Form Logic ---
const stepsContainer_dynamic = document.getElementById('stepsContainer_dynamic');
const addStepBtn_dynamic = document.getElementById('addStepBtn_dynamic');
const stepTemplate_dynamic = document.getElementById('stepTemplate_dynamic');
const requirementTemplate_dynamic = document.getElementById('requirementTemplate_dynamic');
const form_dynamic = document.getElementById('createFlowForm_dynamic');
if (stepsContainer_dynamic && addStepBtn_dynamic && stepTemplate_dynamic && requirementTemplate_dynamic && form_dynamic) { // Check if all elements exist
const updateIndices_dynamic = () => {
const steps = stepsContainer_dynamic.querySelectorAll('.step'); // .step is the class on the root of the cloned template
steps.forEach((step, stepIdx) => {
step.dataset.stepIndex = stepIdx;
step.querySelector('.step-number').textContent = stepIdx + 1;
const descriptionInput = step.querySelector('.step-description_dynamic');
if(descriptionInput) descriptionInput.name = `steps[${stepIdx}].description`;
const addReqBtn = step.querySelector('.addRequirementBtn_dynamic');
if (addReqBtn) addReqBtn.dataset.stepIndex = stepIdx;
const requirements = step.querySelectorAll('.requirementsContainer_dynamic .requirement'); // .requirement is class on root of its template
requirements.forEach((req, reqIdx) => {
req.dataset.reqIndex = reqIdx;
req.querySelector('.req-number').textContent = reqIdx + 1;
const messageTextarea = req.querySelector('.req-message_dynamic');
if(messageTextarea) messageTextarea.name = `steps[${stepIdx}].requirements[${reqIdx}].message`;
const pubkeyInput = req.querySelector('.req-pubkey_dynamic');
if(pubkeyInput) pubkeyInput.name = `steps[${stepIdx}].requirements[${reqIdx}].public_key`;
});
});
// Remove the initial placeholder message if steps are present
const placeholder = stepsContainer_dynamic.querySelector('p.text-muted');
if (steps.length > 0 && placeholder) {
placeholder.style.display = 'none';
} else if (steps.length === 0 && placeholder) {
placeholder.style.display = 'block';
}
};
const addRequirement_dynamic = (currentStepElement, stepIndex) => {
const requirementsContainer = currentStepElement.querySelector('.requirementsContainer_dynamic');
if (!requirementsContainer) return;
const reqFragment = requirementTemplate_dynamic.content.cloneNode(true);
const newRequirement = reqFragment.querySelector('.requirement'); // .requirement is class on root
// Remove placeholder from requirements container if it exists
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
if (reqPlaceholder) reqPlaceholder.style.display = 'none';
requirementsContainer.appendChild(newRequirement);
updateIndices_dynamic();
};
const addStep_dynamic = () => {
const stepFragment = stepTemplate_dynamic.content.cloneNode(true);
const newStep = stepFragment.querySelector('.step'); // .step is class on root
stepsContainer_dynamic.appendChild(newStep);
const currentStepIndex = stepsContainer_dynamic.querySelectorAll('.step').length - 1;
addRequirement_dynamic(newStep, currentStepIndex); // Add one requirement by default
updateIndices_dynamic();
};
stepsContainer_dynamic.addEventListener('click', (event) => {
if (event.target.classList.contains('removeStepBtn_dynamic')) {
event.target.closest('.step').remove();
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
// addStep_dynamic(); // Optionally re-add a step if all are removed
}
updateIndices_dynamic();
} else if (event.target.classList.contains('addRequirementBtn_dynamic')) {
const stepElement = event.target.closest('.step');
const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
addRequirement_dynamic(stepElement, stepIndex);
} else if (event.target.classList.contains('removeRequirementBtn_dynamic')) {
const requirementElement = event.target.closest('.requirement');
const stepElement = event.target.closest('.step');
const requirementsContainer = stepElement.querySelector('.requirementsContainer_dynamic');
requirementElement.remove();
if (requirementsContainer.querySelectorAll('.requirement').length === 0) {
// const stepIndex = parseInt(stepElement.dataset.stepIndex, 10);
// addRequirement_dynamic(stepElement, stepIndex); // Optionally re-add a requirement
const reqPlaceholder = requirementsContainer.querySelector('p.text-muted.small');
if (reqPlaceholder) reqPlaceholder.style.display = 'block'; // Show placeholder if no reqs
}
updateIndices_dynamic();
}
});
addStepBtn_dynamic.addEventListener('click', addStep_dynamic);
// Add one step by default when the page loads, if no steps already (e.g. from server-side render)
if (stepsContainer_dynamic.children.length === 1 && stepsContainer_dynamic.firstElementChild.tagName === 'P') { // Only placeholder present
addStep_dynamic();
}
form_dynamic.addEventListener('submit', (event) => {
if (stepsContainer_dynamic.querySelectorAll('.step').length === 0) {
alert('Please add at least one step to the flow.');
event.preventDefault();
return;
}
const steps = stepsContainer_dynamic.querySelectorAll('.step');
for (let i = 0; i < steps.length; i++) {
if (steps[i].querySelectorAll('.requirementsContainer_dynamic .requirement').length === 0) {
alert(`Step ${i + 1} must have at least one signature requirement.`);
event.preventDefault();
return;
}
}
});
} else {
console.warn('One or more elements for the dynamic step-by-step form were not found. JS not initialized.');
}
// --- End of Dynamic Step-by-Step Form Logic ---
});
</script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Flow from Rhai Script</title>
<link rel="stylesheet" href="/static/style.css">
<style>
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f9;
color: #333;
}
.container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
textarea {
width: 100%;
min-height: 300px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 15px;
font-family: monospace;
font-size: 14px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.back-link {
display: block;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">&larr; Back to Flow List</a>
<h1>Create Flow from Rhai Script</h1>
<div id="rhai_script_examples_data" style="display: none;">
{% for example in example_scripts %}
<div id="rhai_example_content_{{ loop.index }}">{{ example.content }}</div>
{% endfor %}
</div>
<div>
<label for="example_script_selector">Load Example Script:</label>
<select id="example_script_selector">
<option value="">-- Select an Example --</option>
{% for example in example_scripts %}
<option value="{{ example.name }}" data-example-id="rhai_example_content_{{ loop.index }}">{{ example.name }}</option>
{% endfor %}
</select>
</div>
<form action="/flows/create_script" method="POST" style="margin-top: 15px;">
<div>
<label for="rhai_script">Rhai Script:</label>
</div>
<div>
<textarea id="rhai_script" name="rhai_script" placeholder="Enter your Rhai script here or select an example above..."></textarea>
</div>
<button type="submit">Create Flow</button>
</form>
<script>
document.getElementById('example_script_selector').addEventListener('change', function() {
var selectedOption = this.options[this.selectedIndex];
var exampleId = selectedOption.getAttribute('data-example-id');
if (exampleId) {
var scriptContent = document.getElementById(exampleId).textContent; // Use textContent
document.getElementById('rhai_script').value = scriptContent;
} else {
document.getElementById('rhai_script').value = '';
}
});
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,8 @@
// Minimal Single Signature Flow
let flow_id = create_flow("Quick Sign");
let step1_id = add_step(flow_id, "Sign the message", 0);
add_requirement(step1_id, "any_signer_pk", "Please provide your signature.");
print("Minimal Flow (ID: " + flow_id + ") defined.");
()

View File

@@ -0,0 +1,18 @@
// Flow with Multi-Requirement Step
// If create_flow, add_step, or add_requirement fail from Rust,
// the script will stop and the error will be reported by the server.
let flow_id = create_flow("Multi-Req Sign Off");
let step1_id = add_step(flow_id, "Initial Signatures (3 needed)", 0);
add_requirement(step1_id, "signer1_pk", "Signatory 1: Please sign terms.");
add_requirement(step1_id, "signer2_pk", "Signatory 2: Please sign terms.");
add_requirement(step1_id, "signer3_pk", "Signatory 3: Please sign terms.");
let step2_id = add_step(flow_id, "Final Confirmation", 1);
add_requirement(step2_id, "final_approver_pk", "Final approval for multi-req sign off.");
print("Multi-Requirement Flow (ID: " + flow_id + ") defined.");
()

View File

@@ -0,0 +1,14 @@
// Simple Two-Step Flow
// If create_flow, add_step, or add_requirement fail from Rust,
// the script will stop and the error will be reported by the server.
let flow_id = create_flow("Simple Two-Stepper");
let step1_id = add_step(flow_id, "Collect Document", 0);
add_requirement(step1_id, "user_pubkey_document", "Please sign the document hash.");
let step2_id = add_step(flow_id, "Approval Signature", 1);
add_requirement(step2_id, "approver_pubkey", "Please approve the collected document.");
print("Simple Two-Step Flow (ID: " + flow_id + ") defined.");
()

1824
sigsocket/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
sigsocket/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "sigsocket"
version = "0.1.0"
edition = "2021"
description = "WebSocket server for handling signing operations"
[dependencies]
actix = "0.13.0"
actix-web = "4.3.1"
actix-web-actors = "4.2.0"
tokio = { version = "1.28.0", features = ["full"] }
secp256k1 = "0.28.0"
sha2 = "0.10.8"
hex = "0.4.3"
base64 = "0.21.0"
rand = "0.8.5"
thiserror = "1.0.40"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4.17"
env_logger = "0.10.0"
futures = "0.3.28"
uuid = { version = "1.3.3", features = ["v4"] }

80
sigsocket/README.md Normal file
View File

@@ -0,0 +1,80 @@
# SigSocket: WebSocket Signing Server
SigSocket is a WebSocket server that handles cryptographic signing operations. It allows clients to connect via WebSocket, identify themselves with a public key, and sign messages on demand.
## Features
- Accept WebSocket connections from clients
- Allow clients to identify themselves with a secp256k1 public key
- Forward messages to clients for signing
- Verify signatures using the client's public key
- Support for request timeouts
- Clean API for application integration
## Architecture
SigSocket follows a modular architecture with the following components:
1. **SigSocket Manager**: Handles WebSocket connections and manages connection lifecycle
2. **Connection Registry**: Maps public keys to active WebSocket connections
3. **Message Handler**: Processes incoming messages and implements the message protocol
4. **Signature Verifier**: Verifies signatures using secp256k1
5. **SigSocket Service**: Provides a clean API for applications to use
## Message Protocol
The protocol is designed to be simple and efficient:
1. **Client Introduction** (first message after connection):
```
<hex_encoded_public_key>
```
2. **Sign Request** (sent from server to client):
```
<base64_encoded_message>
```
3. **Sign Response** (sent from client to server):
```
<base64_encoded_message>.<base64_encoded_signature>
```
## API Usage
```rust
// Create and initialize the service
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
// Use the service to send a message for signing
async fn sign_message(
service: Arc<SigSocketService>,
public_key: String,
message: Vec<u8>
) -> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
service.send_to_sign(&public_key, &message).await
}
```
## Security Considerations
- All public keys are validated to ensure they are properly formatted secp256k1 keys
- Messages are hashed using SHA-256 before signature verification
- WebSocket connections have heartbeat checks to automatically close inactive connections
- All inputs are validated to prevent injection attacks
## Running the Example Server
Start the example server with:
```bash
RUST_LOG=info cargo run
```
This will launch a server on `127.0.0.1:8080` with the following endpoints:
- `/ws` - WebSocket endpoint for client connections
- `/sign` - HTTP POST endpoint to request message signing
- `/status` - HTTP GET endpoint to check connection count
- `/connected/{public_key}` - HTTP GET endpoint to check if a client is connected

View File

@@ -0,0 +1,71 @@
# SigSocket Examples
This directory contains example applications demonstrating how to use the SigSocket library for cryptographic signing operations using WebSockets.
## Overview
These examples demonstrate a common workflow:
1. **Web Application with Integrated SigSocket Server**: An Actix-based web server that both serves the web UI and runs the SigSocket WebSocket server for handling connections and signing requests.
2. **Client Application**: A web interface that connects to the SigSocket WebSocket endpoint, receives signing requests, and submits signatures.
## Directory Structure
- `web_app/`: The web application with integrated SigSocket server
- `client_app/`: The client application that signs messages
## Running the Examples
You only need to run two components:
### 1. Start the Web Application with Integrated SigSocket Server
Start the web application which also runs the SigSocket server:
```bash
cd /path/to/sigsocket/examples/web_app
cargo run
```
This will start a web interface at http://127.0.0.1:8080 where you can submit messages to be signed. It also starts the SigSocket WebSocket server at ws://127.0.0.1:8080/ws.
### 2. Start the Client Application
The client application connects to the WebSocket endpoint and waits for signing requests:
```bash
cd /path/to/sigsocket/examples/client_app
cargo run
```
This will start a web interface at http://127.0.0.1:8082 where you can see signing requests and approve them.
## Using the Applications
1. Open the client app in a browser at http://127.0.0.1:8082
2. Note the public key displayed on the page
3. Open the web app in another browser window at http://127.0.0.1:8080
4. Enter the public key from step 2 into the "Public Key" field
5. Enter a message to be signed and submit the form
6. The message will be sent to the SigSocket server, which forwards it to the connected client
7. In the client app, you'll see the sign request appear - click "Sign Message" to approve
8. The signature will be sent back through the SigSocket server to the web app
9. The web app will display the signature
## How It Works
1. **SigSocket Server**: Provides a WebSocket endpoint for clients to connect and register with their public keys. It also accepts HTTP requests to sign messages with a specific client's key.
2. **Web Application**:
- Provides a form for users to enter a public key and message
- Uses the SigSocket service to send the message to be signed
- Displays the resulting signature
3. **Client Application**:
- Connects to the SigSocket server via WebSocket
- Registers with a public key
- Waits for signing requests
- Displays incoming requests and allows the user to approve them
- Signs messages using ECDSA with Secp256k1 and sends the signatures back
This demonstrates a real-world use case where a web application needs to verify a user's identity or get approval for transactions through cryptographic signatures, without having direct access to the private keys.

2575
sigsocket/examples/client_app/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
[package]
name = "sigsocket-client-example"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.28.0", features = ["full"] }
tokio-tungstenite = { version = "0.18.0", features = ["native-tls"] }
futures-util = "0.3.28"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
env_logger = "0.10.0"
secp256k1 = { version = "0.26.0", features = ["rand-std"] }
sha2 = "0.10.6"
rand = "0.8.5"
hex = "0.4.3"
base64 = "0.21.2"
actix-web = "4.3.1"
actix-files = "0.6.2"
tera = "1.19.0"
url = "2.4.0"

View File

@@ -0,0 +1,474 @@
use actix_files as fs;
use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result};
use serde::{Deserialize, Serialize};
use tera::{Tera, Context};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tokio_tungstenite::{connect_async, tungstenite};
use futures_util::{StreamExt, SinkExt};
use secp256k1::{Secp256k1, SecretKey, Message};
use sha2::{Sha256, Digest};
use url::Url;
use std::thread;
// Struct for representing a sign request
#[derive(Serialize, Deserialize, Clone, Debug)]
struct SignRequest {
id: String,
message: String,
#[serde(skip)]
message_raw: String, // Original base64 message for sending back in the response
#[serde(skip)]
message_decoded: String, // Decoded message for display
}
// Struct for representing the application state
struct AppState {
templates: Tera,
keypair: Arc<KeyPair>,
pending_request: Arc<Mutex<Option<SignRequest>>>,
websocket_sender: mpsc::Sender<WebSocketCommand>,
}
// Commands that can be sent to the WebSocket connection
enum WebSocketCommand {
Sign { id: String, message: String, signature: Vec<u8> },
Close,
}
// Keypair for signing messages
struct KeyPair {
secret_key: SecretKey,
public_key_hex: String,
}
impl KeyPair {
fn new() -> Self {
let secp = Secp256k1::new();
let mut rng = rand::thread_rng();
// Generate a new random keypair
let (secret_key, public_key) = secp.generate_keypair(&mut rng);
// Convert public key to hex for identification
let public_key_hex = hex::encode(public_key.serialize());
KeyPair {
secret_key,
public_key_hex,
}
}
fn sign(&self, message: &[u8]) -> Vec<u8> {
// Hash the message first (secp256k1 requires a 32-byte hash)
let mut hasher = Sha256::new();
hasher.update(message);
let message_hash = hasher.finalize();
// Create a secp256k1 message from the hash
let secp_message = Message::from_slice(&message_hash).unwrap();
// Sign the message
let secp = Secp256k1::new();
let signature = secp.sign_ecdsa(&secp_message, &self.secret_key);
// Return the serialized signature
signature.serialize_compact().to_vec()
}
}
// Controller for the index page
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
let mut context = Context::new();
// Add the keypair to the context
context.insert("public_key", &data.keypair.public_key_hex);
// Add the pending request if there is one
if let Some(request) = &*data.pending_request.lock().unwrap() {
context.insert("request", request);
}
let rendered = data.templates.render("index.html", &context)
.map_err(|e| {
eprintln!("Template error: {}", e);
actix_web::error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
// Controller for the sign endpoint
async fn sign_request(
data: web::Data<AppState>,
form: web::Form<SignRequestForm>,
) -> impl Responder {
println!("SIGN ENDPOINT: Starting sign_request handler for form ID: {}", form.id);
// Try to get a lock on the pending request
println!("SIGN ENDPOINT: Attempting to acquire lock on pending_request");
match data.pending_request.try_lock() {
Ok(mut guard) => {
// Check if we have a pending request
if let Some(request) = &*guard {
println!("SIGN ENDPOINT: Found pending request with ID: {}", request.id);
// Get the request ID
let id = request.id.clone();
// Verify that the request ID matches
if id == form.id {
println!("SIGN ENDPOINT: Request ID matches form ID: {}", id);
// Sign the message
let message = request.message.as_bytes();
println!("SIGN ENDPOINT: About to sign message: {} (length: {})",
String::from_utf8_lossy(message), message.len());
let signature = data.keypair.sign(message);
println!("SIGN ENDPOINT: Message signed successfully. Signature length: {}", signature.len());
// Send the signature via WebSocket
println!("SIGN ENDPOINT: About to send signature via websocket channel");
match data.websocket_sender.send(WebSocketCommand::Sign {
id: id.clone(),
message: request.message_raw.clone(), // Include the original base64 message
signature
}).await {
Ok(_) => {
println!("SIGN ENDPOINT: Successfully sent signature to websocket channel");
},
Err(e) => {
let error_msg = format!("Failed to send signature: {}", e);
println!("SIGN ENDPOINT ERROR: {}", error_msg);
return HttpResponse::InternalServerError()
.content_type("text/html")
.body(format!("<h1>Error sending signature</h1><p>{}</p><p><a href='/'>Return to home</a></p>", error_msg));
}
}
// Clear the pending request
println!("SIGN ENDPOINT: Clearing pending request");
*guard = None;
// Return a success page that continues to the next step
println!("SIGN ENDPOINT: Returning success response");
return HttpResponse::Ok()
.content_type("text/html")
.body(r#"<html>
<head>
<title>Signature Sent</title>
<meta http-equiv="refresh" content="2; url=/" />
<script type="text/javascript">
console.log("Signature sent successfully, redirecting in 2 seconds...");
setTimeout(function() { window.location.href = '/'; }, 2000);
</script>
<style>
body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; }
.success { color: green; }
</style>
</head>
<body>
<h1 class="success">✓ Signature Sent Successfully!</h1>
<p>Redirecting back to home page...</p>
<p><a href="/">Click here if you're not redirected automatically</a></p>
</body>
</html>"#);
} else {
println!("SIGN ENDPOINT: Request ID {} does not match form ID {}", request.id, form.id);
}
} else {
println!("SIGN ENDPOINT: No pending request found");
}
},
Err(e) => {
let error_msg = format!("Failed to acquire lock on pending_request: {}", e);
println!("SIGN ENDPOINT ERROR: {}", error_msg);
return HttpResponse::InternalServerError()
.content_type("text/html")
.body(format!("<h1>Error processing request</h1><p>{}</p><p><a href='/'>Return to home</a></p>", error_msg));
}
}
// Redirect back to the index page (if no request was found or ID didn't match)
println!("SIGN ENDPOINT: No matching request found, redirecting to home");
HttpResponse::SeeOther()
.append_header(("Location", "/"))
.finish()
}
// Form for submitting a signature
#[derive(Deserialize)]
struct SignRequestForm {
id: String,
}
// WebSocket client task that connects to the SigSocket server
async fn websocket_client_task(
keypair: Arc<KeyPair>,
pending_request: Arc<Mutex<Option<SignRequest>>>,
mut command_receiver: mpsc::Receiver<WebSocketCommand>,
) {
// Connect directly to the web app's integrated SigSocket endpoint
let sigsocket_url = "ws://127.0.0.1:8080/ws";
// Reconnection settings
let mut retry_count = 0;
const MAX_RETRY_COUNT: u32 = 10; // Reset retry counter after this many attempts
const BASE_RETRY_DELAY_MS: u64 = 1000; // Start with 1 second
const MAX_RETRY_DELAY_MS: u64 = 30000; // Cap at 30 seconds
loop {
// Calculate backoff delay with jitter for retry
let delay_ms = if retry_count > 0 {
let base_delay = BASE_RETRY_DELAY_MS * 2u64.pow(retry_count.min(6));
let jitter = rand::random::<u64>() % 500; // Add up to 500ms of jitter
(base_delay + jitter).min(MAX_RETRY_DELAY_MS)
} else {
0 // No delay on first attempt
};
if retry_count > 0 {
println!("Reconnection attempt {} in {} ms...", retry_count, delay_ms);
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
}
// Connect to the SigSocket server with timeout
println!("Connecting to SigSocket server at {}", sigsocket_url);
let connect_result = tokio::time::timeout(
tokio::time::Duration::from_secs(10), // Connection timeout
connect_async(Url::parse(sigsocket_url).unwrap())
).await;
match connect_result {
// Timeout error
Err(_) => {
eprintln!("Connection attempt timed out");
retry_count = (retry_count + 1) % MAX_RETRY_COUNT;
continue;
},
// Connection result
Ok(conn_result) => match conn_result {
// Connection successful
Ok((mut ws_stream, _)) => {
println!("Connected to SigSocket server");
// Reset retry counter on successful connection
retry_count = 0;
// Heartbeat functionality has been removed
println!("DEBUG: Running without heartbeat functionality");
// Send the initial message with just the raw public key
let intro_message = keypair.public_key_hex.clone();
if let Err(e) = ws_stream.send(tungstenite::Message::Text(intro_message)).await {
eprintln!("Failed to send introduction message: {}", e);
continue;
}
println!("Sent introduction with public key: {}", keypair.public_key_hex);
// Last time we received a message or pong from the server
let mut last_server_response = std::time::Instant::now();
// Process incoming messages and commands
loop {
tokio::select! {
// Handle WebSocket message
msg = ws_stream.next() => {
match msg {
Some(Ok(tungstenite::Message::Text(text))) => {
println!("Received message: {}", text);
last_server_response = std::time::Instant::now();
// Parse the message as a sign request
match serde_json::from_str::<SignRequest>(&text) {
Ok(mut request) => {
println!("DEBUG: Successfully parsed sign request with ID: {}", request.id);
println!("DEBUG: Base64 message: {}", request.message);
// Save the original base64 message for later use in response
request.message_raw = request.message.clone();
// Decode the base64 message content
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &request.message) {
Ok(decoded) => {
let decoded_text = String::from_utf8_lossy(&decoded).to_string();
println!("DEBUG: Decoded message: {}", decoded_text);
// Store the decoded message for display
request.message_decoded = decoded_text;
// Update the message for displaying in the UI
request.message = request.message_decoded.clone();
// Store the request for display in the UI
*pending_request.lock().unwrap() = Some(request);
println!("Received signing request. Please check the web UI to approve it.");
},
Err(e) => {
eprintln!("Error decoding base64 message: {}", e);
}
}
},
Err(e) => {
eprintln!("Error parsing sign request JSON: {}", e);
eprintln!("Raw message: {}", text);
}
}
},
Some(Ok(tungstenite::Message::Ping(data))) => {
// Respond to ping with pong
last_server_response = std::time::Instant::now();
if let Err(e) = ws_stream.send(tungstenite::Message::Pong(data)).await {
eprintln!("Failed to send pong: {}", e);
break;
}
},
Some(Ok(tungstenite::Message::Pong(_))) => {
// Got pong response from the server
last_server_response = std::time::Instant::now();
},
Some(Ok(_)) => {
// Ignore other types of messages
last_server_response = std::time::Instant::now();
},
Some(Err(e)) => {
eprintln!("WebSocket error: {}", e);
break;
},
None => {
eprintln!("WebSocket connection closed");
break;
},
}
},
// Heartbeat functionality has been removed
// Handle signing command from the web interface
cmd = command_receiver.recv() => {
match cmd {
Some(WebSocketCommand::Sign { id, message, signature }) => {
println!("DEBUG: Signing request ID: {}", id);
println!("DEBUG: Raw signature bytes: {:?}", signature);
println!("DEBUG: Using message from command: {}", message);
// Convert signature bytes to base64
let sig_base64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature);
println!("DEBUG: Base64 signature: {}", sig_base64);
// Create a JSON response with explicit ID and message/signature fields
let response = format!("{{\"id\": \"{}\", \"message\": \"{}\", \"signature\": \"{}\"}}",
id, message, sig_base64);
println!("DEBUG: Preparing to send JSON response: {}", response);
println!("DEBUG: Response length: {} bytes", response.len());
// Log that we're about to send on the WebSocket connection
println!("DEBUG: About to send on WebSocket connection");
// Send the signature response right away - with extra logging
println!("!!!! ATTEMPTING TO SEND SIGNATURE RESPONSE NOW !!!!");
match ws_stream.send(tungstenite::Message::Text(response.clone())).await {
Ok(_) => {
last_server_response = std::time::Instant::now();
println!("!!!! SUCCESSFULLY SENT SIGNATURE RESPONSE !!!!");
println!("!!!! SIGNATURE SENT FOR REQUEST ID: {} !!!!", id);
// Clear the pending request after successful signature
*pending_request.lock().unwrap() = None;
// Send another simple message to confirm the connection is still working
if let Err(e) = ws_stream.send(tungstenite::Message::Text("CONFIRM_SIGNATURE_SENT".to_string())).await {
println!("DEBUG: Failed to send confirmation message: {}", e);
} else {
println!("DEBUG: Sent confirmation message after signature");
}
},
Err(e) => {
eprintln!("!!!! FAILED TO SEND SIGNATURE RESPONSE: {} !!!!", e);
// Try to reconnect or recover
println!("DEBUG: Attempting to diagnose connection issue...");
break;
}
}
},
Some(WebSocketCommand::Close) => {
println!("DEBUG: Received close command, closing connection");
break;
},
None => {
eprintln!("Command channel closed");
break;
}
}
}
}
}
// Connection loop has ended, will attempt to reconnect
println!("WebSocket connection closed, will attempt to reconnect...");
},
// Connection error
Err(e) => {
eprintln!("Failed to connect to SigSocket server: {}", e);
}
}
}
// Increment retry counter but don't exceed MAX_RETRY_COUNT
retry_count = (retry_count + 1) % MAX_RETRY_COUNT;
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Setup logger
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
// Initialize templates
let mut tera = Tera::default();
tera.add_raw_templates(vec![
("index.html", include_str!("../templates/index.html")),
]).unwrap();
// Generate a keypair for signing
let keypair = Arc::new(KeyPair::new());
println!("Generated keypair with public key: {}", keypair.public_key_hex);
// Create a channel for sending commands to the WebSocket client
let (command_sender, command_receiver) = mpsc::channel::<WebSocketCommand>(32);
// Create the pending request mutex
let pending_request = Arc::new(Mutex::new(None::<SignRequest>));
// Spawn the WebSocket client task
let ws_keypair = keypair.clone();
let ws_pending_request = pending_request.clone();
tokio::spawn(async move {
websocket_client_task(ws_keypair, ws_pending_request, command_receiver).await;
});
// Create the app state
let app_state = web::Data::new(AppState {
templates: tera,
keypair,
pending_request,
websocket_sender: command_sender,
});
println!("Client App server starting on http://127.0.0.1:8082");
// Start the web server
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
// Register routes
.route("/", web::get().to(index))
.route("/sign", web::post().to(sign_request))
// Static files
.service(fs::Files::new("/static", "./static"))
})
.bind("127.0.0.1:8082")?
.run()
.await
}

View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SigSocket Client Demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1, h2 {
color: #333;
text-align: center;
}
.status-box {
text-align: center;
padding: 15px;
margin-bottom: 30px;
border-radius: 5px;
background-color: #f5f5f5;
}
.status-connected {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.client-info {
margin-bottom: 30px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
.keypair-info {
font-family: monospace;
word-break: break-all;
margin: 10px 0;
}
.request-panel {
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 30px;
background-color: #fff;
}
.message-box {
font-family: monospace;
background-color: #f8f9fa;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 15px 0;
white-space: pre-wrap;
word-break: break-all;
}
.no-requests {
text-align: center;
padding: 30px;
color: #6c757d;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
display: block;
margin: 0 auto;
}
button:hover {
background-color: #45a049;
}
.footer {
text-align: center;
margin-top: 30px;
color: #6c757d;
font-size: 0.9em;
}
</style>
</head>
<body>
<h1>SigSocket Client Demo</h1>
<div class="status-box status-connected">
<p><strong>Status:</strong> Connected to SigSocket Server</p>
</div>
<div class="client-info">
<h2>Client Information</h2>
<p><strong>Public Key:</strong></p>
<p class="keypair-info">{{ public_key }}</p>
<p>This public key is used to identify this client to the SigSocket server.</p>
</div>
{% if request %}
<div class="request-panel">
<h2>Pending Sign Request</h2>
<p><strong>Request ID:</strong> {{ request.id }}</p>
<p><strong>Message to Sign:</strong></p>
<div class="message-box">{{ request.message }}</div>
<form action="/sign" method="post">
<input type="hidden" name="id" value="{{ request.id }}">
<button type="submit">Sign Message</button>
</form>
</div>
{% else %}
<div class="request-panel no-requests">
<h2>No Pending Requests</h2>
<p>Waiting for a sign request from the SigSocket server...</p>
</div>
{% endif %}
<div class="footer">
<p>This client connects to a SigSocket server via WebSocket and responds to signature requests.</p>
<p>The signing is done using Secp256k1 ECDSA with a randomly generated keypair.</p>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 start-0 p-3" style="z-index: 11; width: 100%;">
<!-- Toasts will be added here dynamically -->
</div>
<script>
// Override console.log to show toast messages
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
console.log = function(message) {
// Call the original console.log
originalConsoleLog.apply(console, arguments);
// Show toast with the message
showToast(message, 'info');
};
console.error = function(message) {
// Call the original console.error
originalConsoleError.apply(console, arguments);
// Show toast with the error message
showToast(message, 'danger');
};
function showToast(message, type = 'info') {
// Create toast element
const toastId = 'toast-' + Date.now();
const toastElement = document.createElement('div');
toastElement.id = toastId;
toastElement.className = 'toast w-100';
toastElement.setAttribute('role', 'alert');
toastElement.setAttribute('aria-live', 'assertive');
toastElement.setAttribute('aria-atomic', 'true');
// Set toast content
toastElement.innerHTML = `
<div class="toast-header bg-${type} text-white">
<strong class="me-auto">${type === 'danger' ? 'Error' : 'Info'}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
// Append to container
document.querySelector('.toast-container').appendChild(toastElement);
// Initialize and show the toast
const toast = new bootstrap.Toast(toastElement, {
autohide: true,
delay: 5000
});
toast.show();
// Remove toast after it's hidden
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.remove();
});
}
// Test toast
console.log('Client app loaded successfully!');
</script>
</body>
</html>

View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Script to run both the SigSocket web app and client app and open them in the browser
# Set the base directory
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WEB_APP_DIR="$BASE_DIR/web_app"
CLIENT_APP_DIR="$BASE_DIR/client_app"
# Colors for terminal output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to kill background processes on exit
cleanup() {
echo -e "${YELLOW}Stopping all processes...${NC}"
kill $(jobs -p) 2>/dev/null
exit 0
}
# Set up cleanup on script termination
trap cleanup INT TERM EXIT
echo -e "${GREEN}Starting SigSocket Demo Applications...${NC}"
# Start the web app in the background
echo -e "${GREEN}Starting Web App (http://127.0.0.1:8080)...${NC}"
cd "$WEB_APP_DIR" && cargo run &
# Wait for the web app to start (adjust time as needed)
echo "Waiting for web app to initialize..."
sleep 5
# Start the client app in the background
echo -e "${GREEN}Starting Client App (http://127.0.0.1:8082)...${NC}"
cd "$CLIENT_APP_DIR" && cargo run &
# Wait for the client app to start
echo "Waiting for client app to initialize..."
sleep 5
# Open browsers (works on macOS)
echo -e "${GREEN}Opening browsers...${NC}"
open "http://127.0.0.1:8080" # Web App
sleep 1
open "http://127.0.0.1:8082" # Client App
echo -e "${GREEN}SigSocket demo is running!${NC}"
echo -e "${YELLOW}Press Ctrl+C to stop all applications${NC}"
# Keep the script running until Ctrl+C
wait

Some files were not shown because too many files have changed in this diff Show More