Compare commits

29 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
34594b95fa ... 2025-04-22 10:39:29 +04:00
15b05cb599 ... 2025-04-22 08:50:31 +04:00
b6dd04a6aa ... 2025-04-22 07:14:37 +04:00
310a5d956f ... 2025-04-22 06:59:24 +04:00
af4f09a67b ... 2025-04-22 06:44:29 +04:00
093aff3851 ... 2025-04-22 06:08:08 +04:00
4a87392194 ... 2025-04-22 06:07:50 +04:00
Timur Gordon
951af7dec7 implement contracts 2025-04-22 03:06:58 +02:00
Timur Gordon
36d605829f Merge branch 'main' of https://git.ourworld.tf/herocode/hostbasket 2025-04-22 02:16:19 +02:00
Timur Gordon
6ed6737c7e implement governance and flow functionality 2025-04-22 02:15:49 +02:00
143 changed files with 33904 additions and 595 deletions

193
actix_mvc_app/Cargo.lock generated
View File

@@ -107,6 +107,45 @@ dependencies = [
"syn", "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]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.5.3" version = "0.5.3"
@@ -247,6 +286,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-identity", "actix-identity",
"actix-multipart",
"actix-session", "actix-session",
"actix-web", "actix-web",
"bcrypt", "bcrypt",
@@ -255,14 +295,17 @@ dependencies = [
"dotenv", "dotenv",
"env_logger", "env_logger",
"futures", "futures",
"futures-util",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"log", "log",
"num_cpus", "num_cpus",
"pulldown-cmark",
"redis", "redis",
"serde", "serde",
"serde_json", "serde_json",
"tera", "tera",
"urlencoding",
"uuid", "uuid",
] ]
@@ -821,6 +864,41 @@ dependencies = [
"cipher", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.4.0" version = "0.4.0"
@@ -945,6 +1023,22 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.1"
@@ -1075,6 +1169,15 @@ dependencies = [
"version_check", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@@ -1386,6 +1489,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@@ -1553,6 +1662,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.5" version = "0.7.5"
@@ -1749,6 +1864,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]] [[package]]
name = "parse-zoneinfo" name = "parse-zoneinfo"
version = "0.3.1" version = "0.3.1"
@@ -1931,6 +2052,25 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -2122,6 +2262,19 @@ dependencies = [
"semver", "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]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.20" version = "1.0.20"
@@ -2187,6 +2340,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.8" version = "0.6.8"
@@ -2326,6 +2488,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -2354,6 +2522,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "tera" name = "tera"
version = "1.20.0" version = "1.20.0"
@@ -2622,6 +2803,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.6" version = "0.2.6"
@@ -2655,6 +2842,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]] [[package]]
name = "utf16_iter" name = "utf16_iter"
version = "1.0.5" version = "1.0.5"

View File

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

View File

@@ -1,6 +1,6 @@
# Actix MVC App # Zanzibar Digital Freezone
A Rust web application built with Actix Web, Tera templates, and Bootstrap 5.3.5, following the MVC (Model-View-Controller) architectural pattern. Convenience, Safety and Privacy
## Features ## Features
@@ -42,8 +42,8 @@ actix_mvc_app/
1. Clone the repository: 1. Clone the repository:
``` ```
git clone https://github.com/yourusername/actix_mvc_app.git git clone https://github.com/yourusername/zanzibar-digital-freezone.git
cd actix_mvc_app cd zanzibar-digital-freezone
``` ```
2. Build the project: 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

@@ -2,6 +2,7 @@ use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cook
use actix_session::Session; use actix_session::Session;
use tera::Tera; use tera::Tera;
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole}; use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
use crate::utils::render_template;
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey}; use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration}; use chrono::{Utc, Duration};
@@ -91,13 +92,7 @@ impl AuthController {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "login"); ctx.insert("active_page", "login");
let rendered = tmpl.render("auth/login.html", &ctx) render_template(&tmpl, "auth/login.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles user login /// Handles user login
@@ -146,13 +141,7 @@ impl AuthController {
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("active_page", "register"); ctx.insert("active_page", "register");
let rendered = tmpl.render("auth/register.html", &ctx) render_template(&tmpl, "auth/register.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles user registration /// Handles user registration

View File

@@ -6,7 +6,7 @@ use tera::Tera;
use serde_json::Value; use serde_json::Value;
use crate::models::{CalendarEvent, CalendarViewMode}; use crate::models::{CalendarEvent, CalendarViewMode};
use crate::utils::RedisCalendarService; use crate::utils::{RedisCalendarService, render_template};
/// Controller for handling calendar-related routes /// Controller for handling calendar-related routes
pub struct CalendarController; pub struct CalendarController;
@@ -215,13 +215,7 @@ impl CalendarController {
}, },
} }
let rendered = tmpl.render("calendar/index.html", &ctx) render_template(&tmpl, "calendar/index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles the new event page route /// Handles the new event page route
@@ -234,13 +228,7 @@ impl CalendarController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("calendar/new_event.html", &ctx) render_template(&tmpl, "calendar/new_event.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles the create event route /// Handles the create event route
@@ -298,13 +286,9 @@ impl CalendarController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("calendar/new_event.html", &ctx) let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::InternalServerError().content_type("text/html").body(rendered)) Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
} }
} }
} }

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

@@ -0,0 +1,742 @@
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, ContractSigner, ContractRevision, SignerStatus, TocItem};
use crate::utils::render_template;
#[derive(Debug, Deserialize)]
pub struct ContractForm {
pub title: String,
pub description: String,
pub contract_type: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct SignerForm {
pub name: String,
pub email: String,
}
pub struct ContractController;
impl ContractController {
// Display the contracts dashboard
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let stats = ContractStatistics::new(&contracts);
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Add stats
context.insert("stats", &serde_json::to_value(stats).unwrap());
// Add recent contracts
let recent_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.take(5)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("recent_contracts", &recent_contracts);
// Add pending signature contracts
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.filter(|c| c.status == ContractStatus::PendingSignatures)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("pending_signature_contracts", &pending_signature_contracts);
// Add draft contracts
let draft_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.filter(|c| c.status == ContractStatus::Draft)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("draft_contracts", &draft_contracts);
render_template(&tmpl, "contracts/index.html", &context)
}
// Display the list of all contracts
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.map(|c| Self::contract_to_json(c))
.collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
context.insert("contracts", &contracts_data);
context.insert("filter", &"all");
render_template(&tmpl, "contracts/contracts.html", &context)
}
// Display the list of user's contracts
pub async fn my_contracts(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.map(|c| Self::contract_to_json(c))
.collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
context.insert("contracts", &contracts_data);
render_template(&tmpl, "contracts/my_contracts.html", &context)
}
// Display a specific contract
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();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Find the contract by ID
let contracts = Self::get_mock_contracts();
// 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, Error> {
let mut context = Context::new();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Add contract types for dropdown
let contract_types = vec![
("Service", "Service Agreement"),
("Employment", "Employment Contract"),
("NDA", "Non-Disclosure Agreement"),
("SLA", "Service Level Agreement"),
("Other", "Other")
];
context.insert("contract_types", &contract_types);
render_template(&tmpl, "contracts/create_contract.html", &context)
}
// Process the create contract form
pub async fn create(
_tmpl: web::Data<Tera>,
_form: web::Form<ContractForm>,
) -> 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
Ok(HttpResponse::Found().append_header(("Location", "/contracts")).finish())
}
// Helper method to convert Contract to a JSON object for templates
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(contract.status.as_str().to_string()));
map.insert("contract_type".to_string(), serde_json::Value::String(contract.contract_type.as_str().to_string()));
map.insert("created_by".to_string(), serde_json::Value::String(contract.created_by.clone()));
map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()));
map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()));
// Organization info
if let Some(org) = &contract.organization_id {
map.insert("organization".to_string(), serde_json::Value::String(org.clone()));
} else {
map.insert("organization".to_string(), serde_json::Value::Null);
}
// Add signers
let signers: Vec<serde_json::Value> = contract.signers.iter()
.map(|s| {
let mut signer_map = serde_json::Map::new();
signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone()));
signer_map.insert("name".to_string(), serde_json::Value::String(s.name.clone()));
signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone()));
signer_map.insert("status".to_string(), serde_json::Value::String(s.status.as_str().to_string()));
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)
})
.collect();
map.insert("signers".to_string(), serde_json::Value::Array(signers));
// Add pending_signers count for templates
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers)));
// 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| {
let mut revision_map = serde_json::Map::new();
revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(r.version)));
revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone()));
revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()));
revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone()));
if let Some(comments) = &r.comments {
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.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 {
map.insert("effective_date".to_string(), serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()));
}
if let Some(expiration_date) = &contract.expiration_date {
map.insert("expiration_date".to_string(), serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()));
}
map
}
// Generate mock contracts for testing
fn get_mock_contracts() -> Vec<Contract> {
let mut contracts = Vec::new();
// 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,
};
// 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.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()),
});
// 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()),
});
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()),
});
// 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,
};
// 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.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,
});
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,
});
// 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()),
});
// 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![],
},
]),
};
// 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.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,
});
// 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.
// 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,
};
// 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

@@ -0,0 +1,633 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use chrono::{Utc, Duration};
use serde::Deserialize;
use tera::Tera;
use crate::models::flow::{Flow, FlowStatus, FlowType, FlowStatistics, FlowStep, StepStatus, FlowLog};
use crate::controllers::auth::Claims;
use crate::utils::render_template;
pub struct FlowController;
impl FlowController {
/// Renders the flows dashboard
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let flows = Self::get_mock_flows();
let stats = FlowStatistics::new(&flows);
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &flows);
ctx.insert("stats", &stats);
ctx.insert("active_flows", &flows.iter().filter(|f| f.status == FlowStatus::InProgress).collect::<Vec<_>>());
ctx.insert("stuck_flows", &flows.iter().filter(|f| f.status == FlowStatus::Stuck).collect::<Vec<_>>());
render_template(&tmpl, "flows/index.html", &ctx)
}
/// Renders the flows list page
pub async fn list_flows(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let flows = Self::get_mock_flows();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &flows);
render_template(&tmpl, "flows/flows.html", &ctx)
}
/// Renders the flow detail page
pub async fn flow_detail(
path: web::Path<String>,
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
let flow_id = path.into_inner();
let user = Self::get_user_from_session(&session);
// Find the flow with the given ID
let flows = Self::get_mock_flows();
let flow = flows.iter().find(|f| f.id == flow_id);
if let Some(flow) = flow {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flow", flow);
render_template(&tmpl, "flows/flow_detail.html", &ctx)
} else {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("error", "Flow not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)),
Err(e) => {
log::error!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e)))
}
}
}
}
/// Renders the create flow page
pub async fn create_flow_form(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
render_template(&tmpl, "flows/create_flow.html", &ctx)
}
/// Handles the create flow form submission
pub async fn create_flow(
_form: web::Form<FlowForm>,
_session: Session
) -> impl Responder {
// In a real application, we would create a new flow here
// For now, just redirect to the flows list
HttpResponse::Found()
.append_header(("Location", "/flows"))
.finish()
}
/// Renders the my flows page
pub async fn my_flows(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let user = Self::get_user_from_session(&session);
if let Some(user) = &user {
let flows = Self::get_mock_flows();
let my_flows = flows.iter()
.filter(|f| f.owner_name == user.sub)
.collect::<Vec<_>>();
let mut ctx = tera::Context::new();
ctx.insert("active_page", "flows");
ctx.insert("user", &user);
ctx.insert("flows", &my_flows);
render_template(&tmpl, "flows/my_flows.html", &ctx)
} else {
Ok(HttpResponse::Found()
.append_header(("Location", "/login"))
.finish())
}
}
/// Handles the advance flow step action
pub async fn advance_flow_step(
path: web::Path<String>,
_session: Session
) -> impl Responder {
let flow_id = path.into_inner();
// In a real application, we would advance the flow step here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Handles the mark flow step as stuck action
pub async fn mark_flow_step_stuck(
path: web::Path<String>,
_form: web::Form<StuckForm>,
_session: Session
) -> impl Responder {
let flow_id = path.into_inner();
// In a real application, we would mark the flow step as stuck here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Handles the add log to flow step action
pub async fn add_log_to_flow_step(
path: web::Path<(String, String)>,
_form: web::Form<LogForm>,
_session: Session
) -> impl Responder {
let (flow_id, _step_id) = path.into_inner();
// In a real application, we would add a log to the flow step here
// For now, just redirect to the flow detail page
HttpResponse::Found()
.append_header(("Location", format!("/flows/{}", flow_id)))
.finish()
}
/// Gets the user from the session
fn get_user_from_session(session: &Session) -> Option<Claims> {
if let Ok(Some(user)) = session.get::<Claims>("user") {
Some(user)
} else {
None
}
}
/// Creates mock flow data for testing
fn get_mock_flows() -> Vec<Flow> {
let mut flows = Vec::new();
// Create a few mock flows
let mut flow1 = Flow {
id: "flow-1".to_string(),
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: "Ibrahim Faraji".to_string(),
steps: vec![
FlowStep {
id: "step-1-1".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(5)),
completed_at: Some(Utc::now() - Duration::days(4)),
logs: vec![
FlowLog {
id: "log-1-1-1".to_string(),
message: "Initial document package submitted".to_string(),
timestamp: Utc::now() - Duration::days(5),
},
FlowLog {
id: "log-1-1-2".to_string(),
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: "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::days(3)),
completed_at: None,
logs: vec![
FlowLog {
id: "log-1-2-1".to_string(),
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: "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(5),
updated_at: Utc::now() - Duration::days(1),
completed_at: None,
progress_percentage: 40,
current_step: None,
};
// Update the current step
flow1.current_step = flow1.steps.iter().find(|s| s.status == StepStatus::InProgress).cloned();
let mut flow2 = Flow {
id: "flow-2".to_string(),
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: "Amina Salim".to_string(),
steps: vec![
FlowStep {
id: "step-2-1".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(30)),
completed_at: Some(Utc::now() - Duration::days(25)),
logs: vec![
FlowLog {
id: "log-2-1-1".to_string(),
message: "Asset documentation submitted for verification".to_string(),
timestamp: Utc::now() - Duration::days(30),
},
FlowLog {
id: "log-2-1-2".to_string(),
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: "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(24)),
completed_at: Some(Utc::now() - Duration::days(20)),
logs: vec![
FlowLog {
id: "log-2-2-1".to_string(),
message: "Tokenization proposal submitted for review".to_string(),
timestamp: Utc::now() - Duration::days(24),
},
FlowLog {
id: "log-2-2-2".to_string(),
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: "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(19)),
completed_at: Some(Utc::now() - Duration::days(15)),
logs: vec![
FlowLog {
id: "log-2-3-1".to_string(),
message: "Smart contract code submitted for audit".to_string(),
timestamp: Utc::now() - Duration::days(19),
},
FlowLog {
id: "log-2-3-2".to_string(),
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(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: "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-3".to_string(),
owner_name: "Hassan Mwinyi".to_string(),
steps: vec![
FlowStep {
id: "step-3-1".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(15)),
completed_at: Some(Utc::now() - Duration::days(12)),
logs: vec![
FlowLog {
id: "log-3-1-1".to_string(),
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: "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: "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(11)),
completed_at: None,
logs: vec![
FlowLog {
id: "log-3-2-1".to_string(),
message: "Environmental assessment initiated".to_string(),
timestamp: Utc::now() - Duration::days(11),
},
FlowLog {
id: "log-3-2-2".to_string(),
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: "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(15),
updated_at: Utc::now() - Duration::days(7),
completed_at: None,
progress_percentage: 35,
current_step: None,
};
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
}
}
/// Form for creating a new flow
#[derive(Debug, Deserialize)]
pub struct FlowForm {
/// Flow name
pub name: String,
/// Flow description
pub description: String,
/// Flow type
pub flow_type: String,
}
/// Form for marking a step as stuck
#[derive(Debug, Deserialize)]
pub struct StuckForm {
/// Reason for being stuck
pub reason: String,
}
/// Form for adding a log to a step
#[derive(Debug, Deserialize)]
pub struct LogForm {
/// Log message
pub message: String,
}

View File

@@ -0,0 +1,568 @@
use actix_web::{web, HttpResponse, Responder, Result};
use actix_session::Session;
use tera::Tera;
use serde_json::Value;
use serde::{Deserialize, Serialize};
use chrono::{Utc, Duration};
use crate::models::governance::{Proposal, Vote, ProposalStatus, VoteType, VotingResults};
use crate::utils::render_template;
/// Controller for handling governance-related routes
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> {
// 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)
})
}
/// Handles the governance dashboard page route
pub async fn index(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// 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 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();
ctx.insert("stats", &stats);
render_template(&tmpl, "governance/index.html", &ctx)
}
/// Handles the proposal list page route
pub async fn proposals(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "proposals");
// Add user to context if available
if let Some(user) = Self::get_user_from_session(&session) {
ctx.insert("user", &user);
}
// Get mock proposals
let proposals = Self::get_mock_proposals();
ctx.insert("proposals", &proposals);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the proposal detail page route
pub async fn proposal_detail(
path: web::Path<String>,
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
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);
}
// Get mock proposal detail
let proposal = Self::get_mock_proposal_by_id(&proposal_id);
if let Some(proposal) = proposal {
ctx.insert("proposal", &proposal);
// Get mock votes for this proposal
let votes = Self::get_mock_votes_for_proposal(&proposal_id);
ctx.insert("votes", &votes);
// Get voting results
let results = Self::get_mock_voting_results(&proposal_id);
ctx.insert("results", &results);
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
} else {
// Proposal not found
ctx.insert("error", "Proposal not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)),
Err(e) => {
eprintln!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e)))
}
}
}
}
/// Handles the create proposal page route
pub async fn create_proposal_form(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "create");
// 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)
}
/// Handles the submission of a new proposal
pub async fn submit_proposal(
_form: web::Form<ProposalForm>,
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
// 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
ctx.insert("success", "Proposal created successfully!");
// Get mock proposals
let proposals = Self::get_mock_proposals();
ctx.insert("proposals", &proposals);
render_template(&tmpl, "governance/proposals.html", &ctx)
}
/// Handles the submission of a vote on a proposal
pub async fn submit_vote(
path: web::Path<String>,
_form: web::Form<VoteForm>,
tmpl: web::Data<Tera>,
session: Session
) -> Result<impl Responder> {
let proposal_id = path.into_inner();
// 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);
}
// Get mock proposal detail
let proposal = Self::get_mock_proposal_by_id(&proposal_id);
if let Some(proposal) = proposal {
ctx.insert("proposal", &proposal);
ctx.insert("success", "Your vote has been recorded!");
// Get mock votes for this proposal
let votes = Self::get_mock_votes_for_proposal(&proposal_id);
ctx.insert("votes", &votes);
// Get voting results
let results = Self::get_mock_voting_results(&proposal_id);
ctx.insert("results", &results);
render_template(&tmpl, "governance/proposal_detail.html", &ctx)
} else {
// Proposal not found
ctx.insert("error", "Proposal not found");
// For the error page, we'll use a special case to set the status code to 404
match tmpl.render("error.html", &ctx) {
Ok(content) => Ok(HttpResponse::NotFound().content_type("text/html").body(content)),
Err(e) => {
eprintln!("Error rendering error template: {}", e);
Err(actix_web::error::ErrorInternalServerError(format!("Error: {}", e)))
}
}
}
}
/// Handles the my votes page route
pub async fn my_votes(tmpl: web::Data<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "governance");
ctx.insert("active_tab", "my_votes");
// 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
/// Generate mock proposals for testing
fn get_mock_proposals() -> Vec<Proposal> {
let now = Utc::now();
vec![
Proposal {
id: "prop-001".to_string(),
creator_id: 1,
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),
voting_starts_at: Some(now - Duration::days(3)),
voting_ends_at: Some(now + Duration::days(4)),
},
Proposal {
id: "prop-002".to_string(),
creator_id: 2,
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),
voting_starts_at: Some(now - Duration::days(14)),
voting_ends_at: Some(now - Duration::days(2)),
},
Proposal {
id: "prop-003".to_string(),
creator_id: 3,
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),
voting_starts_at: None,
voting_ends_at: None,
},
Proposal {
id: "prop-004".to_string(),
creator_id: 1,
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),
voting_starts_at: Some(now - Duration::days(19)),
voting_ends_at: Some(now - Duration::days(5)),
},
Proposal {
id: "prop-005".to_string(),
creator_id: 4,
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,
},
]
}
/// Get a mock proposal by ID
fn get_mock_proposal_by_id(id: &str) -> Option<Proposal> {
Self::get_mock_proposals().into_iter().find(|p| p.id == id)
}
/// Generate mock votes for a specific proposal
fn get_mock_votes_for_proposal(proposal_id: &str) -> Vec<Vote> {
let now = Utc::now();
vec![
Vote {
id: "vote-001".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 1,
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),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-002".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 2,
voter_name: "Jane Smith".to_string(),
vote_type: VoteType::Yes,
comment: None,
created_at: now - Duration::days(2),
updated_at: now - Duration::days(2),
},
Vote {
id: "vote-003".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 3,
voter_name: "Bob Johnson".to_string(),
vote_type: VoteType::No,
comment: Some("I have concerns about the implementation cost.".to_string()),
created_at: now - Duration::days(1),
updated_at: now - Duration::days(1),
},
Vote {
id: "vote-004".to_string(),
proposal_id: proposal_id.to_string(),
voter_id: 4,
voter_name: "Alice Williams".to_string(),
vote_type: VoteType::Abstain,
comment: Some("I need more information before making a decision.".to_string()),
created_at: now - Duration::hours(12),
updated_at: now - Duration::hours(12),
},
]
}
/// Generate mock votes for a specific user
fn get_mock_votes_for_user(user_id: i32) -> Vec<(Vote, Proposal)> {
let votes = vec![
Vote {
id: "vote-001".to_string(),
proposal_id: "prop-001".to_string(),
voter_id: user_id,
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),
updated_at: Utc::now() - Duration::days(2),
},
Vote {
id: "vote-005".to_string(),
proposal_id: "prop-002".to_string(),
voter_id: user_id,
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),
updated_at: Utc::now() - Duration::days(10),
},
Vote {
id: "vote-008".to_string(),
proposal_id: "prop-004".to_string(),
voter_id: user_id,
voter_name: "Robert Callingham".to_string(),
vote_type: VoteType::Yes,
comment: None,
created_at: Utc::now() - Duration::days(18),
updated_at: Utc::now() - Duration::days(18),
},
Vote {
id: "vote-010".to_string(),
proposal_id: "prop-005".to_string(),
voter_id: user_id,
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),
updated_at: Utc::now() - Duration::days(5),
},
];
let proposals = Self::get_mock_proposals();
votes.into_iter()
.filter_map(|vote| {
proposals.iter()
.find(|p| p.id == vote.proposal_id)
.map(|p| (vote.clone(), p.clone()))
})
.collect()
}
/// Generate mock voting results for a proposal
fn get_mock_voting_results(proposal_id: &str) -> VotingResults {
let votes = Self::get_mock_votes_for_proposal(proposal_id);
let mut results = VotingResults::new(proposal_id.to_string());
for vote in votes {
results.add_vote(&vote.vote_type);
}
results
}
/// Generate mock statistics for the governance dashboard
fn get_mock_statistics() -> GovernanceStats {
GovernanceStats {
total_proposals: 5,
active_proposals: 2,
approved_proposals: 1,
rejected_proposals: 1,
draft_proposals: 1,
total_votes: 15,
participation_rate: 75.0,
}
}
}
/// Represents the data submitted in the proposal form
#[derive(Debug, Deserialize)]
pub struct ProposalForm {
/// Title of the proposal
pub title: String,
/// Description of the proposal
pub description: String,
/// Start date for voting
pub voting_start_date: Option<String>,
/// End date for voting
pub voting_end_date: Option<String>,
}
/// Represents the data submitted in the vote form
#[derive(Debug, Deserialize)]
pub struct VoteForm {
/// Type of vote (yes, no, abstain)
pub vote_type: String,
/// Optional comment explaining the vote
pub comment: Option<String>,
}
/// Represents statistics for the governance dashboard
#[derive(Debug, Serialize)]
pub struct GovernanceStats {
/// Total number of proposals
pub total_proposals: usize,
/// Number of active proposals
pub active_proposals: usize,
/// Number of approved proposals
pub approved_proposals: usize,
/// Number of rejected proposals
pub rejected_proposals: usize,
/// Number of draft proposals
pub draft_proposals: usize,
/// Total number of votes cast
pub total_votes: usize,
/// Participation rate (percentage)
pub participation_rate: f64,
}

View File

@@ -1,8 +1,10 @@
use actix_web::{web, HttpResponse, Responder, Result}; use actix_web::{web, Responder, Result};
use actix_session::Session; use actix_session::Session;
use tera::Tera; use tera::Tera;
use serde_json::Value; use serde_json::Value;
use crate::utils::render_template;
/// Controller for handling home-related routes /// Controller for handling home-related routes
pub struct HomeController; pub struct HomeController;
@@ -24,13 +26,7 @@ impl HomeController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("editor.html", &ctx) render_template(&tmpl, "editor.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles the home page route /// Handles the home page route
@@ -43,13 +39,7 @@ impl HomeController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("index.html", &ctx) render_template(&tmpl, "index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles the about page route /// Handles the about page route
@@ -62,13 +52,7 @@ impl HomeController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("about.html", &ctx) render_template(&tmpl, "about.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles the contact page route /// Handles the contact page route
@@ -81,13 +65,7 @@ impl HomeController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("contact.html", &ctx) render_template(&tmpl, "contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Handles form submissions from the contact page /// Handles form submissions from the contact page
@@ -112,13 +90,7 @@ impl HomeController {
ctx.insert("user", &user); ctx.insert("user", &user);
} }
let rendered = tmpl.render("contact.html", &ctx) render_template(&tmpl, "contact.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
} }

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

@@ -2,4 +2,13 @@
pub mod home; pub mod home;
pub mod auth; pub mod auth;
pub mod ticket; pub mod ticket;
pub mod calendar; 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

@@ -4,6 +4,7 @@ use tera::Tera;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority}; use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority};
use crate::utils::render_template;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -131,13 +132,7 @@ impl TicketController {
]); ]);
// Render the template // Render the template
let rendered = tmpl.render("tickets/list.html", &ctx) render_template(&tmpl, "tickets/list.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Shows the form for creating a new ticket /// Shows the form for creating a new ticket
@@ -172,13 +167,7 @@ impl TicketController {
]); ]);
// Render the template // Render the template
let rendered = tmpl.render("tickets/new.html", &ctx) render_template(&tmpl, "tickets/new.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Creates a new ticket /// Creates a new ticket
@@ -285,13 +274,7 @@ impl TicketController {
]); ]);
// Render the template // Render the template
let rendered = tmpl.render("tickets/show.html", &ctx) render_template(&tmpl, "tickets/show.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
/// Adds a comment to a ticket /// Adds a comment to a ticket
@@ -443,12 +426,6 @@ impl TicketController {
ctx.insert("my_tickets", &true); ctx.insert("my_tickets", &true);
// Render the template // Render the template
let rendered = tmpl.render("tickets/list.html", &ctx) render_template(&tmpl, "tickets/list.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
} }
} }

View File

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

@@ -0,0 +1,314 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Contract status enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractStatus {
Draft,
PendingSignatures,
Signed,
Active,
Expired,
Cancelled
}
impl ContractStatus {
pub fn as_str(&self) -> &str {
match self {
ContractStatus::Draft => "Draft",
ContractStatus::PendingSignatures => "Pending Signatures",
ContractStatus::Signed => "Signed",
ContractStatus::Active => "Active",
ContractStatus::Expired => "Expired",
ContractStatus::Cancelled => "Cancelled",
}
}
}
/// Contract type enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractType {
Service,
Employment,
NDA,
SLA,
Partnership,
Distribution,
License,
Membership,
Other
}
impl ContractType {
pub fn as_str(&self) -> &str {
match self {
ContractType::Service => "Service Agreement",
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",
}
}
}
/// Contract signer status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SignerStatus {
Pending,
Signed,
Rejected
}
impl SignerStatus {
pub fn as_str(&self) -> &str {
match self {
SignerStatus::Pending => "Pending",
SignerStatus::Signed => "Signed",
SignerStatus::Rejected => "Rejected",
}
}
}
/// Contract signer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractSigner {
pub id: String,
pub name: String,
pub email: String,
pub status: SignerStatus,
pub signed_at: Option<DateTime<Utc>>,
pub comments: Option<String>,
}
impl ContractSigner {
/// Creates a new contract signer
pub fn new(name: String, email: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
email,
status: SignerStatus::Pending,
signed_at: None,
comments: None,
}
}
/// Signs the contract
pub fn sign(&mut self, comments: Option<String>) {
self.status = SignerStatus::Signed;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
/// Rejects the contract
pub fn reject(&mut self, comments: Option<String>) {
self.status = SignerStatus::Rejected;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
}
/// Contract revision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractRevision {
pub version: u32,
pub content: String,
pub created_at: DateTime<Utc>,
pub created_by: String,
pub comments: Option<String>,
}
impl ContractRevision {
/// Creates a new contract revision
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
Self {
version,
content,
created_at: Utc::now(),
created_by,
comments,
}
}
}
/// 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 {
pub id: String,
pub title: String,
pub description: String,
pub contract_type: ContractType,
pub status: ContractStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
pub effective_date: Option<DateTime<Utc>>,
pub expiration_date: Option<DateTime<Utc>>,
pub signers: Vec<ContractSigner>,
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 {
/// Creates a new contract
pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
title,
description,
contract_type,
status: ContractStatus::Draft,
created_at: Utc::now(),
updated_at: Utc::now(),
created_by,
effective_date: None,
expiration_date: None,
signers: Vec::new(),
revisions: Vec::new(),
current_version: 1,
organization_id,
content_dir: None,
toc: None,
}
}
/// Adds a signer to the contract
pub fn add_signer(&mut self, name: String, email: String) {
let signer = ContractSigner::new(name, email);
self.signers.push(signer);
self.updated_at = Utc::now();
}
/// Adds a revision to the contract
pub fn add_revision(&mut self, content: String, created_by: String, comments: Option<String>) {
let new_version = self.current_version + 1;
let revision = ContractRevision::new(new_version, content, created_by, comments);
self.revisions.push(revision);
self.current_version = new_version;
self.updated_at = Utc::now();
}
/// Sends the contract for signatures
pub fn send_for_signatures(&mut self) -> Result<(), String> {
if self.revisions.is_empty() {
return Err("Cannot send contract without content".to_string());
}
if self.signers.is_empty() {
return Err("Cannot send contract without signers".to_string());
}
self.status = ContractStatus::PendingSignatures;
self.updated_at = Utc::now();
Ok(())
}
/// Checks if all signers have signed
pub fn is_fully_signed(&self) -> bool {
if self.signers.is_empty() {
return false;
}
self.signers.iter().all(|signer| signer.status == SignerStatus::Signed)
}
/// Marks the contract as signed if all signers have signed
pub fn finalize_if_signed(&mut self) -> bool {
if self.is_fully_signed() {
self.status = ContractStatus::Signed;
self.updated_at = Utc::now();
true
} else {
false
}
}
/// Cancels the contract
pub fn cancel(&mut self) {
self.status = ContractStatus::Cancelled;
self.updated_at = Utc::now();
}
/// Gets the latest revision
pub fn latest_revision(&self) -> Option<&ContractRevision> {
self.revisions.last()
}
/// Gets a specific revision
pub fn get_revision(&self, version: u32) -> Option<&ContractRevision> {
self.revisions.iter().find(|r| r.version == version)
}
/// Gets the number of pending signers
pub fn pending_signers_count(&self) -> usize {
self.signers.iter().filter(|s| s.status == SignerStatus::Pending).count()
}
/// Gets the number of signed signers
pub fn signed_signers_count(&self) -> usize {
self.signers.iter().filter(|s| s.status == SignerStatus::Signed).count()
}
/// Gets the number of rejected signers
pub fn rejected_signers_count(&self) -> usize {
self.signers.iter().filter(|s| s.status == SignerStatus::Rejected).count()
}
}
/// Contract filter for listing contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractFilter {
pub status: Option<ContractStatus>,
pub contract_type: Option<ContractType>,
pub created_by: Option<String>,
pub organization_id: Option<String>,
}
/// Contract statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractStatistics {
pub total_contracts: usize,
pub draft_contracts: usize,
pub pending_signature_contracts: usize,
pub signed_contracts: usize,
pub expired_contracts: usize,
pub cancelled_contracts: usize,
}
impl ContractStatistics {
/// Creates new contract statistics from a list of contracts
pub fn new(contracts: &[Contract]) -> Self {
let total_contracts = contracts.len();
let draft_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Draft).count();
let pending_signature_contracts = contracts.iter().filter(|c| c.status == ContractStatus::PendingSignatures).count();
let signed_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Signed).count();
let expired_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Expired).count();
let cancelled_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Cancelled).count();
Self {
total_contracts,
draft_contracts,
pending_signature_contracts,
signed_contracts,
expired_contracts,
cancelled_contracts,
}
}
}

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

@@ -0,0 +1,387 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Status of a flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FlowStatus {
/// Flow is in progress
InProgress,
/// Flow is completed
Completed,
/// Flow is stuck at a step
Stuck,
/// Flow is cancelled
Cancelled,
}
impl std::fmt::Display for FlowStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowStatus::InProgress => write!(f, "In Progress"),
FlowStatus::Completed => write!(f, "Completed"),
FlowStatus::Stuck => write!(f, "Stuck"),
FlowStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
/// Type of flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FlowType {
/// Company registration flow
CompanyRegistration,
/// User onboarding flow
UserOnboarding,
/// Service activation flow
ServiceActivation,
/// Payment processing flow
PaymentProcessing,
/// Asset tokenization flow
AssetTokenization,
/// Certification flow
Certification,
/// License application flow
LicenseApplication,
}
impl std::fmt::Display for FlowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowType::CompanyRegistration => write!(f, "Company Registration"),
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"),
}
}
}
/// Filter for flows
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FlowFilter {
/// All flows
All,
/// Only in progress flows
InProgress,
/// Only completed flows
Completed,
/// Only stuck flows
Stuck,
/// Only cancelled flows
Cancelled,
/// Flows of a specific type
ByType(FlowType),
}
impl std::fmt::Display for FlowFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlowFilter::All => write!(f, "All"),
FlowFilter::InProgress => write!(f, "In Progress"),
FlowFilter::Completed => write!(f, "Completed"),
FlowFilter::Stuck => write!(f, "Stuck"),
FlowFilter::Cancelled => write!(f, "Cancelled"),
FlowFilter::ByType(flow_type) => write!(f, "Type: {}", flow_type),
}
}
}
/// A step in a flow
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStep {
/// Step ID
pub id: String,
/// Step name
pub name: String,
/// Step description
pub description: String,
/// Step status
pub status: StepStatus,
/// Step order in the flow
pub order: u32,
/// Step started at
pub started_at: Option<DateTime<Utc>>,
/// Step completed at
pub completed_at: Option<DateTime<Utc>>,
/// Step logs
pub logs: Vec<FlowLog>,
}
impl FlowStep {
/// Creates a new flow step
pub fn new(name: String, description: String, order: u32) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
description,
status: StepStatus::Pending,
order,
started_at: None,
completed_at: None,
logs: Vec::new(),
}
}
/// Starts the step
pub fn start(&mut self) {
self.status = StepStatus::InProgress;
self.started_at = Some(Utc::now());
self.add_log("Step started".to_string());
}
/// Completes the step
pub fn complete(&mut self) {
self.status = StepStatus::Completed;
self.completed_at = Some(Utc::now());
self.add_log("Step completed".to_string());
}
/// Marks the step as stuck
pub fn mark_stuck(&mut self, reason: String) {
self.status = StepStatus::Stuck;
self.add_log(format!("Step stuck: {}", reason));
}
/// Adds a log entry to the step
pub fn add_log(&mut self, message: String) {
self.logs.push(FlowLog::new(message));
}
}
/// Status of a step in a flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum StepStatus {
/// Step is pending
Pending,
/// Step is in progress
InProgress,
/// Step is completed
Completed,
/// Step is stuck
Stuck,
/// Step is skipped
Skipped,
}
impl std::fmt::Display for StepStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StepStatus::Pending => write!(f, "Pending"),
StepStatus::InProgress => write!(f, "In Progress"),
StepStatus::Completed => write!(f, "Completed"),
StepStatus::Stuck => write!(f, "Stuck"),
StepStatus::Skipped => write!(f, "Skipped"),
}
}
}
/// A log entry in a flow step
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowLog {
/// Log ID
pub id: String,
/// Log message
pub message: String,
/// Log timestamp
pub timestamp: DateTime<Utc>,
}
impl FlowLog {
/// Creates a new flow log
pub fn new(message: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
message,
timestamp: Utc::now(),
}
}
}
/// A flow with multiple steps
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Flow {
/// Flow ID
pub id: String,
/// Flow name
pub name: String,
/// Flow description
pub description: String,
/// Flow type
pub flow_type: FlowType,
/// Flow status
pub status: FlowStatus,
/// Flow owner ID
pub owner_id: String,
/// Flow owner name
pub owner_name: String,
/// Flow created at
pub created_at: DateTime<Utc>,
/// Flow updated at
pub updated_at: DateTime<Utc>,
/// Flow completed at
pub completed_at: Option<DateTime<Utc>>,
/// Flow steps
pub steps: Vec<FlowStep>,
/// Progress percentage
pub progress_percentage: u8,
/// Current step
pub current_step: Option<FlowStep>,
}
impl Flow {
/// Creates a new flow
pub fn new(name: &str, description: &str, flow_type: FlowType, owner_id: &str, owner_name: &str) -> Self {
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let steps = vec![
FlowStep::new("Initialization".to_string(), "Setting up the flow".to_string(), 1),
FlowStep::new("Processing".to_string(), "Processing the flow data".to_string(), 2),
FlowStep::new("Finalization".to_string(), "Completing the flow".to_string(), 3),
];
// Set the first step as in progress
let mut flow = Self {
id,
name: name.to_string(),
description: description.to_string(),
flow_type,
status: FlowStatus::InProgress,
owner_id: owner_id.to_string(),
owner_name: owner_name.to_string(),
steps,
created_at: now,
updated_at: now,
completed_at: None,
progress_percentage: 0,
current_step: None,
};
// Calculate progress and set current step
flow.update_progress();
flow
}
fn update_progress(&mut self) {
// Calculate progress percentage
let total_steps = self.steps.len();
if total_steps == 0 {
self.progress_percentage = 100;
return;
}
let completed_steps = self.steps.iter().filter(|s| s.status == StepStatus::Completed).count();
self.progress_percentage = ((completed_steps as f32 / total_steps as f32) * 100.0) as u8;
// Find current step
self.current_step = self.steps.iter()
.find(|s| s.status == StepStatus::InProgress)
.cloned();
// Update flow status based on steps
if self.progress_percentage == 100 {
self.status = FlowStatus::Completed;
} else if self.steps.iter().any(|s| s.status == StepStatus::Stuck) {
self.status = FlowStatus::Stuck;
} else {
self.status = FlowStatus::InProgress;
}
}
pub fn advance_step(&mut self) -> Result<(), String> {
let current_index = self.steps.iter().position(|s| s.status == StepStatus::InProgress);
if let Some(index) = current_index {
// Mark current step as completed
self.steps[index].status = StepStatus::Completed;
self.steps[index].completed_at = Some(Utc::now());
// If there's a next step, mark it as in progress
if index + 1 < self.steps.len() {
self.steps[index + 1].status = StepStatus::InProgress;
self.steps[index + 1].started_at = Some(Utc::now());
}
self.updated_at = Utc::now();
self.update_progress();
Ok(())
} else {
Err("No step in progress to advance".to_string())
}
}
pub fn mark_step_stuck(&mut self, reason: &str) -> Result<(), String> {
let current_index = self.steps.iter().position(|s| s.status == StepStatus::InProgress);
if let Some(index) = current_index {
// Mark current step as stuck
self.steps[index].status = StepStatus::Stuck;
// Add a log entry for the stuck reason
self.steps[index].add_log(reason.to_string());
self.updated_at = Utc::now();
self.update_progress();
Ok(())
} else {
Err("No step in progress to mark as stuck".to_string())
}
}
pub fn add_log_to_step(&mut self, step_id: &str, message: &str) -> Result<(), String> {
if let Some(step) = self.steps.iter_mut().find(|s| s.id == step_id) {
step.add_log(message.to_string());
self.updated_at = Utc::now();
Ok(())
} else {
Err(format!("Step with ID {} not found", step_id))
}
}
}
/// Flow statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStatistics {
/// Total number of flows
pub total_flows: usize,
/// Number of in progress flows
pub in_progress_flows: usize,
/// Number of completed flows
pub completed_flows: usize,
/// Number of stuck flows
pub stuck_flows: usize,
/// Number of cancelled flows
pub cancelled_flows: usize,
}
impl FlowStatistics {
/// Creates new flow statistics
pub fn new(flows: &[Flow]) -> Self {
let total_flows = flows.len();
let in_progress_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::InProgress)
.count();
let completed_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Completed)
.count();
let stuck_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Stuck)
.count();
let cancelled_flows = flows.iter()
.filter(|flow| flow.status == FlowStatus::Cancelled)
.count();
Self {
total_flows,
in_progress_flows,
completed_flows,
stuck_flows,
cancelled_flows,
}
}
}

View File

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

View File

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

@@ -2,8 +2,16 @@
pub mod user; pub mod user;
pub mod ticket; pub mod ticket;
pub mod calendar; 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 // Re-export models for easier imports
pub use user::User; pub use user::User;
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter}; pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
pub use calendar::{CalendarEvent, CalendarViewMode}; 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] #[test]
fn test_new_user() { fn test_new_user() {
let user = User::new("John Doe".to_string(), "john@example.com".to_string()); let user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
assert_eq!(user.name, "John Doe"); assert_eq!(user.name, "Robert Callingham");
assert_eq!(user.email, "john@example.com"); assert_eq!(user.email, "john@example.com");
assert!(!user.is_admin()); assert!(!user.is_admin());
} }
@@ -161,13 +161,13 @@ mod tests {
#[test] #[test]
fn test_update_user() { fn test_update_user() {
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string()); let mut user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
user.update(Some("Jane Doe".to_string()), None); user.update(Some("Mary Hewell".to_string()), None);
assert_eq!(user.name, "Jane Doe"); assert_eq!(user.name, "Mary Hewell");
assert_eq!(user.email, "john@example.com"); assert_eq!(user.email, "john@example.com");
user.update(None, Some("jane@example.com".to_string())); 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"); assert_eq!(user.email, "jane@example.com");
} }
} }

View File

@@ -4,6 +4,13 @@ use crate::controllers::home::HomeController;
use crate::controllers::auth::AuthController; use crate::controllers::auth::AuthController;
use crate::controllers::ticket::TicketController; use crate::controllers::ticket::TicketController;
use crate::controllers::calendar::CalendarController; 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::middleware::JwtAuth;
use crate::SESSION_KEY; use crate::SESSION_KEY;
@@ -52,6 +59,89 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/calendar/events/new", web::get().to(CalendarController::new_event)) .route("/calendar/events/new", web::get().to(CalendarController::new_event))
.route("/calendar/events", web::post().to(CalendarController::create_event)) .route("/calendar/events", web::post().to(CalendarController::create_event))
.route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event)) .route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
// Governance routes
.route("/governance", web::get().to(GovernanceController::index))
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
// Flow routes
.service(
web::scope("/flows")
.route("", web::get().to(FlowController::index))
.route("/list", web::get().to(FlowController::list_flows))
.route("/{id}", web::get().to(FlowController::flow_detail))
.route("/{id}/advance", web::post().to(FlowController::advance_flow_step))
.route("/{id}/stuck", web::post().to(FlowController::mark_flow_step_stuck))
.route("/{id}/step/{step_id}/log", web::post().to(FlowController::add_log_to_flow_step))
.route("/create", web::get().to(FlowController::create_flow_form))
.route("/create", web::post().to(FlowController::create_flow))
.route("/my-flows", web::get().to(FlowController::my_flows))
)
// Contract routes
.service(
web::scope("/contracts")
.route("", web::get().to(ContractController::index))
.route("/list", web::get().to(ContractController::list))
.route("/my", web::get().to(ContractController::my_contracts))
.route("/{id}", web::get().to(ContractController::detail))
.route("/create", web::get().to(ContractController::create_form))
.route("/create", web::post().to(ContractController::create))
)
// 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 // 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,5 +1,7 @@
use actix_web::{error, Error, HttpResponse};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use tera::{self, Function, Result, Value}; use tera::{self, Context, Function, Tera, Value};
use std::error::Error as StdError;
// Export modules // Export modules
pub mod redis_service; pub mod redis_service;
@@ -7,6 +9,22 @@ pub mod redis_service;
// Re-export for easier imports // Re-export for easier imports
pub use redis_service::RedisCalendarService; pub use redis_service::RedisCalendarService;
/// Error type for template rendering
#[derive(Debug)]
pub struct TemplateError {
pub message: String,
pub details: String,
pub location: String,
}
impl std::fmt::Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Template error in {}: {}", self.location, self.message)
}
}
impl std::error::Error for TemplateError {}
/// Registers custom Tera functions /// Registers custom Tera functions
pub fn register_tera_functions(tera: &mut tera::Tera) { pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction); tera.register_function("now", NowFunction);
@@ -18,7 +36,7 @@ pub fn register_tera_functions(tera: &mut tera::Tera) {
pub struct NowFunction; pub struct NowFunction;
impl Function for NowFunction { impl Function for NowFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
let format = match args.get("format") { let format = match args.get("format") {
Some(val) => match val.as_str() { Some(val) => match val.as_str() {
Some(s) => s, Some(s) => s,
@@ -43,7 +61,7 @@ impl Function for NowFunction {
pub struct FormatDateFunction; pub struct FormatDateFunction;
impl Function for FormatDateFunction { impl Function for FormatDateFunction {
fn call(&self, args: &std::collections::HashMap<String, Value>) -> Result<Value> { fn call(&self, args: &std::collections::HashMap<String, Value>) -> tera::Result<Value> {
let timestamp = match args.get("timestamp") { let timestamp = match args.get("timestamp") {
Some(val) => match val.as_i64() { Some(val) => match val.as_i64() {
Some(ts) => ts, Some(ts) => ts,
@@ -96,6 +114,89 @@ pub fn truncate_string(s: &str, max_length: usize) -> String {
} }
} }
/// Renders a template with error handling
///
/// This function attempts to render a template and handles any errors by rendering
/// the error template with detailed error information.
pub fn render_template(
tmpl: &Tera,
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) => {
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);
// 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")
);
println!("DEBUG: Returning simple error page");
Ok(HttpResponse::InternalServerError()
.content_type("text/html")
.body(error_html))
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -1,14 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}About - Actix MVC App{% endblock %} {% block title %}About - Zanzibar Digital Freezone{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h1 class="card-title">About Actix MVC App</h1> <h1 class="card-title">About Zanzibar Digital Freezone</h1>
<p class="card-text">This is a sample application demonstrating how to build a web application using Rust with an MVC architecture.</p> <p class="card-text">Convenience, Safety and Privacy</p>
<h2 class="mt-4">Technology Stack</h2> <h2 class="mt-4">Technology Stack</h2>
<div class="row"> <div class="row">

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

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Login - Actix MVC App{% endblock %} {% block title %}Login{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">

File diff suppressed because it is too large Load Diff

View File

@@ -3,48 +3,96 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Actix MVC App{% endblock %}</title> <title>{% block title %}Zanzibar Digital Freezone{% endblock %}</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css"> <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://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">
<style>
/* Minimal custom CSS that can't be achieved with Bootstrap classes */
body {
padding-top: 50px; /* Height of the fixed header */
}
.header {
height: 50px;
position: fixed;
top: 0;
width: 100%;
z-index: 1030;
}
.footer {
height: 40px;
line-height: 40px;
}
@media (min-width: 768px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px); /* Subtract header and footer height */
top: 50px; /* Position below header */
}
.main-content {
margin-left: 240px;
min-height: calc(100vh - 90px);
}
}
@media (max-width: 767.98px) {
.sidebar {
width: 240px;
position: fixed;
height: calc(100vh - 90px);
top: 50px;
left: -240px;
transition: left 0.3s ease;
z-index: 1020;
}
.sidebar.show {
left: 0;
}
}
</style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <!-- Header - Full Width -->
<div class="container"> <header class="header bg-dark text-white">
<a class="navbar-brand" href="/">Actix MVC App</a> <div class="d-flex container-fluid justify-content-between align-items-center h-100">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <div class="align-items-center">
<span class="navbar-toggler-icon"></span> <button class="navbar-toggler d-md-none me-2" type="button" id="sidebarToggle" aria-label="Toggle navigation">
</button> <i class="bi bi-list text-white"></i>
<div class="collapse navbar-collapse" id="navbarNav"> </button>
<ul class="navbar-nav me-auto"> <h5 class="mb-0">Zanzibar Digital Freezone {% if entity_name %}| <span class="text-info">{{ entity_name }}</span>{% endif %}</h5>
<li class="nav-item"> </div>
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a> <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>
<li class="nav-item"> <li class="nav-item mx-3">
<a class="nav-link {% if active_page == 'about' %}active{% endif %}" href="/about">About</a> <a class="nav-link text-white {% if active_page == 'contact' %}active{% endif %}" href="/contact">
</li> Contact
<li class="nav-item"> </a>
<a class="nav-link {% if active_page == 'contact' %}active{% endif %}" href="/contact">Contact</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'tickets' %}active{% endif %}" href="/tickets">Support Tickets</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'calendar' %}active{% endif %}" href="/calendar">Calendar</a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav ms-auto"> </div>
<div>
<ul class="navbar-nav flex-row">
{% if user and user.id %} {% if user and user.id %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ user.name }} <i class="bi bi-person-circle"></i> {{ user.name }}
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown"> <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="/tickets/new">New Ticket</a></li>
<li><a class="dropdown-item" href="/tickets/my">My Tickets</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" %} {% if user.role == "Admin" %}
<li><a class="dropdown-item" href="/admin">Admin Panel</a></li> <li><a class="dropdown-item" href="/admin">Admin Panel</a></li>
{% endif %} {% endif %}
@@ -53,39 +101,163 @@
</ul> </ul>
</li> </li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item me-2">
<a class="nav-link {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a> <a class="nav-link text-white {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if active_page == 'register' %}active{% endif %}" href="/register">Register</a> <a class="nav-link text-white {% if active_page == 'register' %}active{% endif %}" href="/register">Register</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
</div> </div>
</nav> </header>
<main class="container py-4"> <div class="d-flex flex-column min-vh-100">
{% block content %}{% endblock %} <!-- Sidebar -->
</main> <div class="sidebar bg-light shadow-sm border-end d-flex" id="sidebar">
<div class="py-2">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'home' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/">
<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
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'flows' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/flows">
<i class="bi bi-diagram-3 me-2"></i> Flows
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'contracts' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/contracts">
<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>
</ul>
</div>
</div>
<footer class="bg-dark text-white py-4 mt-5"> <!-- Main Content -->
<div class="container"> <div class="main-content flex-grow-1">
<div class="row"> <!-- Page Content -->
<div class="col-md-6"> <main class="py-3 w-100 d-block">
<h5>Actix MVC App</h5> <div class="container-fluid">
<p>A Rust web application using Actix Web, Tera templates, and Bootstrap.</p> {% block content %}{% endblock %}
</div> </div>
<div class="col-md-6 text-md-end"> </main>
<p>&copy; {{ now(year=true) }} Actix MVC App. All rights reserved.</p> </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>
</div> </div>
</div> </footer>
</footer> </div>
<!-- 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 class="toast-body">
{{ success }}
</div>
</div>
{% 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="/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.min.js"></script>
<script src="https://unpkg.com/unpoly@3.7.2/unpoly-bootstrap5.min.js"></script> <script src="https://unpkg.com/unpoly@3.7.2/unpoly-bootstrap5.min.js"></script>
<script>
// Toggle sidebar on mobile
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 %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -3,7 +3,7 @@
{% block title %}Calendar{% endblock %} {% block title %}Calendar{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container-fluid">
<h1>Calendar</h1> <h1>Calendar</h1>
<p>View Mode: {{ view_mode }}</p> <p>View Mode: {{ view_mode }}</p>

View File

@@ -3,7 +3,7 @@
{% block title %}New Calendar Event{% endblock %} {% block title %}New Calendar Event{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container-fluid">
<h1>Create New Event</h1> <h1>Create New Event</h1>
{% if error %} {% if error %}

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" %} {% extends "base.html" %}
{% block title %}Contact - Actix MVC App{% endblock %} {% block title %}Contact - Zanzibar Digital Freezone{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@@ -37,15 +37,15 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Email</h5> <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>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">GitHub</h5> <h5 class="card-title">Website</h5>
<p class="card-text">github.com/example/actix-mvc-app</p> <p class="card-text">https://info.ourworld.tf/zdfz</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,600 @@
{% import "contracts/macros/contract_macros.html" as contract_macros %}
{% extends "base.html" %}
{% block title %}Contract Details{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
<li class="breadcrumb-item"><a href="/contracts/list">All Contracts</a></li>
<li class="breadcrumb-item active" aria-current="page">Contract Details</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">{{ contract.title }}</h1>
<div class="btn-group">
<a href="/contracts/list" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Back to List
</a>
</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>
<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>
<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>
</div>
</div>
<div class="col-md-3">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-primary">
<i class="bi bi-pencil me-1"></i> Edit Contract
</a>
<a href="/contracts/{{ contract.id }}/send" class="btn btn-success">
<i class="bi bi-send me-1"></i> Send for Signatures
</a>
<button class="btn btn-danger">
<i class="bi bi-trash me-1"></i> Delete Contract
</button>
{% elif contract.status == 'PendingSignatures' %}
<button class="btn btn-success">
<i class="bi bi-pen me-1"></i> Sign Contract
</button>
<button class="btn btn-warning">
<i class="bi bi-x-circle me-1"></i> Reject Contract
</button>
<button class="btn btn-secondary">
<i class="bi bi-send me-1"></i> Resend Invitations
</button>
{% elif contract.status == 'Signed' or contract.status == 'Active' %}
<button class="btn btn-primary">
<i class="bi bi-download me-1"></i> Download Contract
</button>
<button class="btn btn-outline-secondary">
<i class="bi bi-files me-1"></i> Clone Contract
</button>
{% endif %}
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Signers Status</h5>
</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
{% for signer in contract.signers %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div>{{ signer.name }}</div>
<small class="text-muted">{{ signer.email }}</small>
</div>
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
{{ signer.status }}
</span>
</li>
{% endfor %}
</ul>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between">
<span>Total: {{ contract.signers|length }}</span>
<span>Signed: {{ signed_signers }} / Pending: {{ pending_signers }}</span>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Contract Info</h5>
</div>
<div class="card-body">
<p><strong>Status:</strong>
<span class="badge {% if contract.status == 'Signed' or contract.status == 'Active' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
{{ contract.status }}
</span>
</p>
<p><strong>Type:</strong> {{ contract.contract_type }}</p>
<p><strong>Created:</strong> {{ contract.created_at }}</p>
<p><strong>Version:</strong> {{ contract.current_version }}</p>
{% if contract.effective_date %}
<p><strong>Effective:</strong> {{ contract.effective_date }}</p>
{% endif %}
{% if contract.expiration_date %}
<p><strong>Expires:</strong> {{ contract.expiration_date }}</p>
{% endif %}
{% if contract.organization %}
<p><strong>Organization:</strong> {{ contract.organization }}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- 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>
</div>
<!-- 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">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>Created By</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.created_by }}</td>
<td>{{ revision.notes }}</td>
<td>
<a href="/contracts/{{ contract.id }}?version={{ revision.version }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<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,140 @@
{% extends "base.html" %}
{% block title %}All Contracts{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">All Contracts</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">All Contracts</h1>
<div class="btn-group">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
</div>
<div class="card-body">
<form action="/contracts/list" method="get" class="row g-3">
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
<option value="Draft">Draft</option>
<option value="PendingSignatures">Pending Signatures</option>
<option value="Signed">Signed</option>
<option value="Expired">Expired</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
<div class="col-md-3">
<label for="type" class="form-label">Contract Type</label>
<select class="form-select" id="type" name="type">
<option value="">All Types</option>
<option value="Service">Service Agreement</option>
<option value="Employment">Employment Contract</option>
<option value="NDA">Non-Disclosure Agreement</option>
<option value="SLA">Service Level Agreement</option>
<option value="Other">Other</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 by title or description">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Apply Filters</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Contract List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contracts</h5>
</div>
<div class="card-body">
{% if contracts and contracts | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Status</th>
<th>Created By</th>
<th>Signers</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>
<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>
</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.signed_signers }}/{{ contract.signers|length }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>{{ contract.updated_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-file-earmark-text fs-1 text-muted"></i>
<p class="mt-3 text-muted">No contracts found</p>
<a href="/contracts/create" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,166 @@
{% extends "base.html" %}
{% block title %}Create New Contract{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Create New Contract</li>
</ol>
</nav>
<h1 class="display-5 mb-3">Create New Contract</h1>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contract Details</h5>
</div>
<div class="card-body">
<form action="/contracts/create" method="post">
<div class="mb-3">
<label for="title" class="form-label">Contract Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="contract_type" class="form-label">Contract Type <span class="text-danger">*</span></label>
<select class="form-select" id="contract_type" name="contract_type" required>
<option value="" selected disabled>Select a contract type</option>
{% for type in contract_types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description <span class="text-danger">*</span></label>
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
</div>
<div class="mb-3">
<label for="content" class="form-label">Contract Content</label>
<textarea class="form-control" id="content" name="content" rows="10"></textarea>
<div class="form-text">You can leave this blank and add content later.</div>
</div>
<div class="mb-3">
<label for="effective_date" class="form-label">Effective Date</label>
<input type="date" class="form-control" id="effective_date" name="effective_date">
</div>
<div class="mb-3">
<label for="expiration_date" class="form-label">Expiration Date</label>
<input type="date" class="form-control" id="expiration_date" name="expiration_date">
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/contracts" class="btn btn-outline-secondary me-md-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Contract</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Tips</h5>
</div>
<div class="card-body">
<p>Creating a new contract is just the first step. After creating the contract, you'll be able to:</p>
<ul>
<li>Add signers who need to approve the contract</li>
<li>Edit the contract content</li>
<li>Send the contract for signatures</li>
<li>Track the signing progress</li>
</ul>
<p>The contract will be in <strong>Draft</strong> status until you send it for signatures.</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contract Templates</h5>
</div>
<div class="card-body">
<p>You can use one of our pre-defined templates to get started quickly:</p>
<div class="list-group">
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('nda')">
Non-Disclosure Agreement
</button>
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('service')">
Service Agreement
</button>
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('employment')">
Employment Contract
</button>
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('sla')">
Service Level Agreement
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function loadTemplate(type) {
// In a real application, this would load template content from the server
let title = '';
let description = '';
let content = '';
let contractType = '';
switch(type) {
case 'nda':
title = 'Non-Disclosure Agreement';
description = 'Standard NDA for protecting confidential information';
contractType = 'Non-Disclosure Agreement';
content = 'This Non-Disclosure Agreement (the "Agreement") is entered into as of [DATE] by and between [PARTY A] and [PARTY B].\n\n1. Definition of Confidential Information\n2. Obligations of Receiving Party\n3. Term\n...';
break;
case 'service':
title = 'Service Agreement';
description = 'Agreement for providing professional services';
contractType = 'Service Agreement';
content = 'This Service Agreement (the "Agreement") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Services to be Provided\n2. Compensation\n3. Term and Termination\n...';
break;
case 'employment':
title = 'Employment Contract';
description = 'Standard employment agreement';
contractType = 'Employment Contract';
content = 'This Employment Agreement (the "Agreement") is made and entered into as of [DATE] by and between [EMPLOYER] and [EMPLOYEE].\n\n1. Position and Duties\n2. Compensation and Benefits\n3. Term and Termination\n...';
break;
case 'sla':
title = 'Service Level Agreement';
description = 'Agreement defining service levels and metrics';
contractType = 'Service Level Agreement';
content = 'This Service Level Agreement (the "SLA") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Service Levels\n2. Performance Metrics\n3. Remedies for Failure\n...';
break;
}
document.getElementById('title').value = title;
document.getElementById('description').value = description;
document.getElementById('content').value = content;
// Set the select option
const selectElement = document.getElementById('contract_type');
for(let i = 0; i < selectElement.options.length; i++) {
if(selectElement.options[i].text === contractType) {
selectElement.selectedIndex = i;
break;
}
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,187 @@
{% extends "base.html" %}
{% block title %}Contracts Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-3">Contracts Dashboard</h1>
<p class="lead">Manage legal agreements and contracts across your organization.</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-2 mb-3">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<h5 class="card-title">Total</h5>
<p class="display-4">{{ stats.total_contracts }}</p>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body">
<h5 class="card-title">Draft</h5>
<p class="display-4">{{ stats.draft_contracts }}</p>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card text-white bg-warning h-100">
<div class="card-body">
<h5 class="card-title">Pending</h5>
<p class="display-4">{{ stats.pending_signature_contracts }}</p>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body">
<h5 class="card-title">Signed</h5>
<p class="display-4">{{ stats.signed_contracts }}</p>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body">
<h5 class="card-title">Expired</h5>
<p class="display-4">{{ stats.expired_contracts }}</p>
</div>
</div>
</div>
<div class="col-md-2 mb-3">
<div class="card text-white bg-dark h-100">
<div class="card-body">
<h5 class="card-title">Cancelled</h5>
<p class="display-4">{{ stats.cancelled_contracts }}</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Quick Actions</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
<a href="/contracts/list" class="btn btn-outline-secondary">
<i class="bi bi-list me-1"></i> View All Contracts
</a>
<a href="/contracts/my-contracts" class="btn btn-outline-secondary">
<i class="bi bi-person me-1"></i> My Contracts
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Pending Signature Contracts -->
{% if pending_signature_contracts and pending_signature_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Pending Signature ({{ pending_signature_contracts|length }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Pending Signers</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in pending_signature_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.pending_signers }} of {{ contract.signers|length }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Draft Contracts -->
{% if draft_contracts and draft_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Draft Contracts</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in draft_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% 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,134 @@
{% extends "base.html" %}
{% block title %}My Contracts{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">My Contracts</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">My Contracts</h1>
<div class="btn-group">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Filters</h5>
</div>
<div class="card-body">
<form action="/contracts/my-contracts" method="get" class="row g-3">
<div class="col-md-4">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
<option value="Draft">Draft</option>
<option value="PendingSignatures">Pending Signatures</option>
<option value="Signed">Signed</option>
<option value="Expired">Expired</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
<div class="col-md-4">
<label for="type" class="form-label">Contract Type</label>
<select class="form-select" id="type" name="type">
<option value="">All Types</option>
<option value="Service">Service Agreement</option>
<option value="Employment">Employment Contract</option>
<option value="NDA">Non-Disclosure Agreement</option>
<option value="SLA">Service Level Agreement</option>
<option value="Other">Other</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Apply Filters</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Contract List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">My Contracts</h5>
</div>
<div class="card-body">
{% if contracts and contracts | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Status</th>
<th>Signers</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>
<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>
</td>
<td>{{ contract.signed_signers }}/{{ contract.signers|length }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>{{ contract.updated_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-file-earmark-text fs-1 text-muted"></i>
<p class="mt-3 text-muted">You don't have any contracts yet</p>
<a href="/contracts/create" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i> Create Your First Contract
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

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

@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Error{% endblock %}
{% block content %}
<div class="row justify-content-center mt-5">
<div class="col-md-8">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h4 class="mb-0"><i class="bi bi-exclamation-triangle-fill me-2"></i>Template Rendering Error</h4>
</div>
<div class="card-body">
<h5 class="card-title">Something went wrong while rendering the template</h5>
<div class="alert alert-danger">
<p class="mb-2"><strong>Error Message:</strong></p>
<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 %}
<div class="mt-3">
<p class="mb-2"><strong>Error Details:</strong></p>
<pre class="p-3 bg-light border rounded"><code>{{ error_details }}</code></pre>
</div>
{% endif %}
{% if error_location is defined and error_location %}
<div class="mt-3">
<p class="mb-2"><strong>Error Location:</strong></p>
<p>{{ error_location }}</p>
</div>
{% endif %}
<div class="alert alert-info mt-3">
<p class="mb-0"><i class="bi bi-info-circle me-2"></i>This error is visible only in development mode. In production, a generic error page will be shown.</p>
</div>
<div class="mt-4">
<a href="/" class="btn btn-primary me-2">
<i class="bi bi-house-door me-1"></i>Go to Home
</a>
<a href="javascript:history.back()" class="btn btn-outline-secondary me-2">
<i class="bi bi-arrow-left me-1"></i>Go Back
</a>
<button onclick="window.location.reload()" class="btn btn-outline-primary">
<i class="bi bi-arrow-clockwise me-1"></i>Reload Page
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% block title %}Create New Flow{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/flows">Flows</a></li>
<li class="breadcrumb-item active" aria-current="page">Create New Flow</li>
</ol>
</nav>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-3">Create New Flow</h1>
<p class="lead">Start a new workflow process by filling out the form below.</p>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form action="/flows/create" method="post">
<!-- Flow Information -->
<div class="mb-4">
<h5>Flow Information</h5>
<hr>
<div class="mb-3">
<label for="name" class="form-label">Flow Name</label>
<input type="text" class="form-control" id="name" name="name" required>
<div class="form-text">A descriptive name for the flow process.</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 class="form-text">Detailed description of the flow's purpose and expected outcome.</div>
</div>
<div class="mb-3">
<label for="flow_type" class="form-label">Flow Type</label>
<select class="form-select" id="flow_type" name="flow_type" required>
<option value="" selected disabled>Select a flow type</option>
<option value="CompanyRegistration">Company Registration</option>
<option value="UserOnboarding">User Onboarding</option>
<option value="ServiceActivation">Service Activation</option>
<option value="PaymentProcessing">Payment Processing</option>
</select>
<div class="form-text">The type of workflow process.</div>
</div>
</div>
<!-- Flow Steps -->
<div class="mb-4">
<h5>Flow Steps</h5>
<hr>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i> Steps will be configured after creating the flow.
</div>
</div>
<!-- Submit Button -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/flows" class="btn btn-outline-secondary me-md-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Flow</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Flow Types</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6>Company Registration</h6>
<p class="small text-muted">Process for registering a new company, including document submission, verification, and approval.</p>
</div>
<div class="mb-3">
<h6>User Onboarding</h6>
<p class="small text-muted">Process for onboarding new users to the platform, including account setup and verification.</p>
</div>
<div class="mb-3">
<h6>Service Activation</h6>
<p class="small text-muted">Process for activating a service, including subscription selection and payment processing.</p>
</div>
<div class="mb-3">
<h6>Payment Processing</h6>
<p class="small text-muted">Process for handling payments, including verification, processing, and receipt generation.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,246 @@
{% extends "base.html" %}
{% block title %}{{ flow.name }} - Flow Details{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/flows">Flows</a></li>
<li class="breadcrumb-item"><a href="/flows/list">All Flows</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ flow.name }}</li>
</ol>
</nav>
</div>
</div>
<!-- Success message if present -->
{% if success %}
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
{% endif %}
<!-- Flow Overview -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">{{ flow.name }}</h4>
<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 %} p-2">
{{ flow.status }}
</span>
</div>
<div class="card-body">
<div class="mb-3">
<h5>Description</h5>
<p>{{ flow.description }}</p>
</div>
<div class="row mb-3">
<div class="col-md-6">
<h5>Details</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between">
<span>Flow Type:</span>
<span class="fw-bold">{{ flow.flow_type }}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Owner:</span>
<span class="fw-bold">{{ flow.owner_name }}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Created:</span>
<span class="fw-bold">{{ flow.created_at | date(format="%Y-%m-%d %H:%M") }}</span>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Last Updated:</span>
<span class="fw-bold">{{ flow.updated_at | date(format="%Y-%m-%d %H:%M") }}</span>
</li>
{% if flow.completed_at %}
<li class="list-group-item d-flex justify-content-between">
<span>Completed:</span>
<span class="fw-bold">{{ flow.completed_at | date(format="%Y-%m-%d %H:%M") }}</span>
</li>
{% endif %}
</ul>
</div>
<div class="col-md-6">
<h5>Progress</h5>
<div class="progress mb-3" style="height: 25px;">
<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>
<p>
<strong>Current Step:</strong>
{% set current = flow.current_step %}
{% if current %}
{{ current.name }}
{% else %}
{% if flow.status == 'Completed' %}
All steps completed
{% elif flow.status == 'Cancelled' %}
Flow cancelled
{% else %}
No active step
{% endif %}
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Actions</h5>
</div>
<div class="card-body">
{% if flow.status == 'In Progress' %}
<div class="d-grid gap-2 mb-3">
<form action="/flows/{{ flow.id }}/advance" method="post" id="advance">
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-arrow-right-circle me-1"></i> Advance to Next Step
</button>
</form>
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-warning w-100" data-bs-toggle="modal" data-bs-target="#markStuckModal">
<i class="bi bi-exclamation-triangle me-1"></i> Mark as Stuck
</button>
</div>
{% elif flow.status == 'Stuck' %}
<div class="d-grid gap-2">
<form action="/flows/{{ flow.id }}/advance" method="post">
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-arrow-right-circle me-1"></i> Resume Flow
</button>
</form>
</div>
{% else %}
<p class="text-center text-muted">No actions available for {{ flow.status | lower }} flows.</p>
{% endif %}
</div>
</div>
{% if flow.steps | length > 0 %}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Add Log to Current Step</h5>
</div>
<div class="card-body">
{% set current = flow.current_step %}
{% if current %}
<form action="/flows/{{ flow.id }}/step/{{ current.id }}/log" method="post">
<div class="mb-3">
<label for="message" class="form-label">Log Message</label>
<textarea class="form-control" id="message" name="message" rows="3" required></textarea>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Add Log Entry
</button>
</div>
</form>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Flow Steps -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Flow Steps</h5>
</div>
<div class="card-body">
<div class="flow-steps">
{% for step in flow.steps %}
<div class="flow-step mb-4">
<div class="card {% if step.status == 'In Progress' %}border-primary{% elif step.status == 'Completed' %}border-success{% elif step.status == 'Stuck' %}border-danger{% else %}border-secondary{% endif %}">
<div class="card-header d-flex justify-content-between align-items-center {% if step.status == 'In Progress' %}bg-primary text-white{% elif step.status == 'Completed' %}bg-success text-white{% elif step.status == 'Stuck' %}bg-danger text-white{% else %}{% endif %}">
<h5 class="mb-0">Step {{ step.order + 1 }}: {{ step.name }}</h5>
<span class="badge {% if step.status == 'In Progress' %}bg-light text-primary{% elif step.status == 'Completed' %}bg-light text-success{% elif step.status == 'Stuck' %}bg-light text-danger{% else %}bg-light text-secondary{% endif %}">
{{ step.status }}
</span>
</div>
<div class="card-body">
<p>{{ step.description }}</p>
<div class="d-flex justify-content-between small text-muted mb-3">
{% if step.started_at %}
<span>Started: {{ step.started_at | date(format="%Y-%m-%d %H:%M") }}</span>
{% else %}
<span>Not started yet</span>
{% endif %}
{% if step.completed_at %}
<span>Completed: {{ step.completed_at | date(format="%Y-%m-%d %H:%M") }}</span>
{% endif %}
</div>
{% if step.logs|length > 0 %}
<h6>Logs</h6>
<div class="logs-container border rounded p-2 bg-light" style="max-height: 200px; overflow-y: auto;">
{% for log in step.logs %}
<div class="log-entry mb-2 pb-2 {% if not loop.last %}border-bottom{% endif %}">
<div class="d-flex justify-content-between">
<span class="fw-bold">{{ log.timestamp | date(format="%Y-%m-%d %H:%M:%S") }}</span>
<span class="text-muted small">ID: {{ log.id }}</span>
</div>
<p class="mb-0">{{ log.message }}</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No logs for this step.</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Mark as Stuck Modal -->
<div class="modal fade" id="markStuckModal" tabindex="-1" aria-labelledby="markStuckModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="/flows/{{ flow.id }}/stuck" method="post">
<div class="modal-header">
<h5 class="modal-title" id="markStuckModalLabel">Mark Flow as Stuck</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="reason" class="form-label">Reason</label>
<textarea class="form-control" id="reason" name="reason" rows="3" required></textarea>
<div class="form-text">Please provide a detailed explanation of why this flow is stuck.</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-warning">Mark as Stuck</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,175 @@
{% extends "base.html" %}
{% block title %}Freezone Workflows{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/flows">Workflows</a></li>
<li class="breadcrumb-item active" aria-current="page">Freezone Workflows</li>
</ol>
</nav>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<h1 class="display-5 mb-0">Freezone Workflow Management</h1>
</div>
<div class="col-md-4 text-md-end">
<a href="/flows/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Workflow
</a>
</div>
</div>
<!-- Filter Controls -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<form class="row g-3" action="/flows/list" 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/list" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> Clear Filters
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Flows Table -->
<div class="row">
<div class="col-12">
<div class="card">
<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

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

@@ -0,0 +1,271 @@
{% 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>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 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>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>
</div>
{% endblock %}

View File

@@ -0,0 +1,116 @@
{% extends "base.html" %}
{% block title %}My Flows{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/flows">Flows</a></li>
<li class="breadcrumb-item active" aria-current="page">My Flows</li>
</ol>
</nav>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<h1 class="display-5 mb-0">My Flows</h1>
</div>
<div class="col-md-4 text-md-end">
<a href="/flows/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Flow
</a>
</div>
</div>
<!-- Flows Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
{% if flows|length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Flow Name</th>
<th>Type</th>
<th>Status</th>
<th>Progress</th>
<th>Current Step</th>
<th>Created</th>
<th>Updated</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>
{% set current = flow.current_step %}
{% if current %}
{{ current.name }}
{% else %}
{% if flow.status == 'Completed' %}
All steps completed
{% elif flow.status == 'Cancelled' %}
Flow cancelled
{% else %}
No active step
{% endif %}
{% endif %}
</td>
<td>{{ flow.created_at | date(format="%Y-%m-%d") }}</td>
<td>{{ flow.updated_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<a href="/flows/{{ flow.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
{% if flow.status == 'In Progress' %}
<a href="/flows/{{ flow.id }}#advance" class="btn btn-sm btn-success">
<i class="bi bi-arrow-right"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-diagram-3 display-1 text-muted"></i>
<h4 class="mt-3">You don't have any flows yet</h4>
<p class="text-muted">Create a new flow to get started with tracking your processes.</p>
<a href="/flows/create" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i> Create New Flow
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}Create Proposal - Governance Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-4">Create Governance Proposal</h1>
<p class="lead">Submit a new proposal for the community to vote on.</p>
</div>
</div>
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" href="/governance">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/proposals">All Proposals</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/my-votes">My Votes</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/governance/create">Create Proposal</a>
</li>
</ul>
</div>
</div>
<!-- Proposal Form -->
<div class="row mb-4">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h5 class="mb-0">New Proposal</h5>
</div>
<div class="card-body">
<form action="/governance/create" method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" required
placeholder="Enter a clear, concise title for your proposal">
<div class="form-text">Make it descriptive and specific</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="6" required
placeholder="Provide a detailed description of your proposal..."></textarea>
<div class="form-text">Explain the purpose, benefits, and implementation details</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="voting_start_date" class="form-label">Voting Start Date</label>
<input type="date" class="form-control" id="voting_start_date" name="voting_start_date">
<div class="form-text">When should voting begin?</div>
</div>
<div class="col-md-6">
<label for="voting_end_date" class="form-label">Voting End Date</label>
<input type="date" class="form-control" id="voting_end_date" name="voting_end_date">
<div class="form-text">When should voting end?</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="draft" name="draft" value="true">
<label class="form-check-label" for="draft">
Save as draft (not ready for voting yet)
</label>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Submit Proposal</button>
<a href="/governance" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Guidelines Card -->
<div class="row mb-4">
<div class="col-md-8 mx-auto">
<div class="card bg-light">
<div class="card-header">
<h5 class="mb-0">Proposal Guidelines</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item bg-transparent">
<strong>Be specific:</strong> Clearly state what you're proposing and why.
</li>
<li class="list-group-item bg-transparent">
<strong>Provide context:</strong> Explain the current situation and why change is needed.
</li>
<li class="list-group-item bg-transparent">
<strong>Consider implementation:</strong> Outline how your proposal could be implemented.
</li>
<li class="list-group-item bg-transparent">
<strong>Address concerns:</strong> Anticipate potential objections and address them.
</li>
<li class="list-group-item bg-transparent">
<strong>Be respectful:</strong> Focus on ideas, not individuals or groups.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,172 @@
{% extends "base.html" %}
{% block title %}Governance 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="/governance">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/proposals">All Proposals</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/my-votes">My Votes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/create">Create Proposal</a>
</li>
</ul>
</div>
</div>
<!-- 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">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">
<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>
</div>
<!-- Recent Proposals Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Active Proposals (Ending Soon)</h5>
</div>
<div class="card-body">
<div class="row">
{% set count = 0 %}
{% for proposal in proposals %}
{% if count < 3 %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{{ proposal.title }}</h5>
<h6 class="card-subtitle mb-2 text-muted">By {{ proposal.creator_name }}</h6>
<p class="card-text">{{ proposal.description | truncate(length=100) }}</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ proposal.status }}
</span>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-outline-primary">View Details</a>
</div>
</div>
<div class="card-footer text-muted text-center">
<span>Voting ends: {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}</span>
</div>
</div>
</div>
{% set count = count + 1 %}
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,135 @@
{% extends "base.html" %}
{% block title %}My Votes - Governance Dashboard{% endblock %}
{% block content %}
<!-- Navigation Tabs -->
<div class="row mb-4">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" href="/governance">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/proposals">All Proposals</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/governance/my-votes">My Votes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/create">Create Proposal</a>
</li>
</ul>
</div>
</div>
<!-- My Votes List -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">My Voting History</h5>
</div>
<div class="card-body">
{% if votes | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Proposal</th>
<th>My Vote</th>
<th>Status</th>
<th>Voted On</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
<tr>
<td>{{ proposal.title }}</td>
<td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ vote.vote_type }}
</span>
</td>
<td>
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
{{ proposal.status }}
</span>
</td>
<td>{{ vote.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View Proposal</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
<h5>You haven't voted on any proposals yet</h5>
<p class="text-muted">When you vote on proposals, they will appear here.</p>
<a href="/governance/proposals" class="btn btn-primary mt-3">Browse Proposals</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Voting Stats -->
{% if votes | length > 0 %}
<div class="row mb-4">
<div class="col-md-4 mb-3">
<div class="card text-white bg-success h-100">
<div class="card-body text-center">
<h5 class="card-title">Yes Votes</h5>
<p class="display-4">
{% set yes_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Yes' %}
{% set yes_count = yes_count + 1 %}
{% endif %}
{% endfor %}
{{ yes_count }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body text-center">
<h5 class="card-title">No Votes</h5>
<p class="display-4">
{% set no_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'No' %}
{% set no_count = no_count + 1 %}
{% endif %}
{% endfor %}
{{ no_count }}
</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card text-white bg-secondary h-100">
<div class="card-body text-center">
<h5 class="card-title">Abstain Votes</h5>
<p class="display-4">
{% set abstain_count = 0 %}
{% for item in votes %}{% set vote = item.0 %}{% set proposal = item.1 %}
{% if vote.vote_type == 'Abstain' %}
{% set abstain_count = abstain_count + 1 %}
{% endif %}
{% endfor %}
{{ abstain_count }}
</p>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,189 @@
{% extends "base.html" %}
{% block title %}{{ proposal.title }} - Governance Proposal{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/governance">Governance</a></li>
<li class="breadcrumb-item"><a href="/governance/proposals">Proposals</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ proposal.title }}</li>
</ol>
</nav>
</div>
</div>
<!-- Success message if present -->
{% if success %}
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
{% endif %}
<!-- Proposal Details -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">{{ proposal.title }}</h4>
</div>
<div class="card-body">
<div class="d-flex justify-content-between mb-3">
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %} p-2">
{{ proposal.status }}
</span>
<small class="text-muted">Created by {{ proposal.creator_name }} on {{ proposal.created_at | date(format="%Y-%m-%d") }}</small>
</div>
<h5>Description</h5>
<p class="mb-4">{{ proposal.description }}</p>
<h5>Voting Period</h5>
<p>
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
<strong>Start:</strong> {{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} <br>
<strong>End:</strong> {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
{% else %}
Not set
{% endif %}
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Voting Results</h5>
</div>
<div class="card-body">
<div class="mb-3">
{% set yes_percent = 0 %}
{% set no_percent = 0 %}
{% set abstain_percent = 0 %}
{% if results.total_votes > 0 %}
{% set yes_percent = (results.yes_count * 100 / results.total_votes) | int %}
{% set no_percent = (results.no_count * 100 / results.total_votes) | int %}
{% set abstain_percent = (results.abstain_count * 100 / results.total_votes) | int %}
{% endif %}
<p class="mb-1">Yes: {{ results.yes_count }} ({{ yes_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ yes_percent }}%"></div>
</div>
<p class="mb-1">No: {{ results.no_count }} ({{ no_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ no_percent }}%"></div>
</div>
<p class="mb-1">Abstain: {{ results.abstain_count }} ({{ abstain_percent }}%)</p>
<div class="progress mb-3">
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{ abstain_percent }}%"></div>
</div>
</div>
<p class="text-center"><strong>Total Votes:</strong> {{ results.total_votes }}</p>
</div>
</div>
<!-- Vote Form -->
{% if proposal.status == "Active" and user and user.id %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">Cast Your Vote</h5>
</div>
<div class="card-body">
<form action="/governance/proposals/{{ proposal.id }}/vote" method="post">
<div class="mb-3">
<label class="form-label">Vote Type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteYes" value="Yes" checked>
<label class="form-check-label" for="voteYes">
Yes - I support this proposal
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteNo" value="No">
<label class="form-check-label" for="voteNo">
No - I oppose this proposal
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="vote_type" id="voteAbstain" value="Abstain">
<label class="form-check-label" for="voteAbstain">
Abstain - I choose not to vote
</label>
</div>
</div>
<div class="mb-3">
<label for="comment" class="form-label">Comment (Optional)</label>
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="Explain your vote..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">Submit Vote</button>
</form>
</div>
</div>
{% elif not user or not user.id %}
<div class="card">
<div class="card-body text-center">
<p>You must be logged in to vote.</p>
<a href="/login" class="btn btn-primary">Login to Vote</a>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Votes List -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Votes ({{ votes | length }})</h5>
</div>
<div class="card-body">
{% if votes | length > 0 %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Voter</th>
<th>Vote</th>
<th>Comment</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{% for vote in votes %}
<tr>
<td>{{ vote.voter_name }}</td>
<td>
<span class="badge {% if vote.vote_type == 'Yes' %}bg-success{% elif vote.vote_type == 'No' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ vote.vote_type }}
</span>
</td>
<td>{% if vote.comment %}{{ vote.comment }}{% else %}No comment{% endif %}</td>
<td>{{ vote.created_at | date(format="%Y-%m-%d %H:%M") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center">No votes have been cast yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block title %}Proposals - Governance Dashboard{% endblock %}
{% block content %}
<!-- Success message if present -->
{% if success %}
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ success }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
{% endif %}
<!-- Navigation Tabs -->
<div class="row mb-3">
<div class="col-12">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" href="/governance">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/governance/proposals">All Proposals</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/my-votes">My Votes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/governance/create">Create Proposal</a>
</li>
</ul>
</div>
</div>
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i> About Proposals</h5>
<p>Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals.</p>
<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">
<div class="card">
<div class="card-body">
<form action="/governance/proposals" method="get" class="row g-3">
<div class="col-md-4">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
<option value="Draft">Draft</option>
<option value="Active">Active</option>
<option value="Approved">Approved</option>
<option value="Rejected">Rejected</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
<div class="col-md-6">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Filter</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Proposals List -->
<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">All Proposals</h5>
<a href="/governance/create" class="btn btn-sm btn-primary">Create New Proposal</a>
</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>Created</th>
<th>Voting Period</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for proposal in proposals %}
<tr>
<td>{{ proposal.title }}</td>
<td>{{ proposal.creator_name }}</td>
<td>
<span class="badge {% if proposal.status == 'Active' %}bg-success{% elif proposal.status == 'Approved' %}bg-primary{% elif proposal.status == 'Rejected' %}bg-danger{% elif proposal.status == 'Draft' %}bg-secondary{% else %}bg-warning{% endif %}">
{{ proposal.status }}
</span>
</td>
<td>{{ proposal.created_at | date(format="%Y-%m-%d") }}</td>
<td>
{% if proposal.voting_starts_at and proposal.voting_ends_at %}
{{ proposal.voting_starts_at | date(format="%Y-%m-%d") }} to {{ proposal.voting_ends_at | date(format="%Y-%m-%d") }}
{% else %}
Not set
{% endif %}
</td>
<td>
<a href="/governance/proposals/{{ proposal.id }}" class="btn btn-sm btn-primary">View</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,53 +1,84 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Home - Actix MVC App{% endblock %} {# Updated template with card blocks - 2025-04-22 #}
{% block title %}Home - Zanzibar Digital Freezone{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h1 class="card-title">Welcome to Actix MVC App</h1> <h1 class="card-title text-center mb-4">Zanzibar Digital Freezone</h1>
<p class="card-text">This is a Rust web application built with:</p> <p class="card-text text-center lead mb-5">Convenience, Safety and Privacy</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">Actix Web - A powerful, pragmatic, and extremely fast web framework for Rust</li> <style>
<li class="list-group-item">Tera Templates - A template engine inspired by Jinja2 and Django templates</li> .compact-card {
<li class="list-group-item">Bootstrap 5.3.5 - A popular CSS framework for responsive web design</li> max-height: 150px;
</ul> overflow-y: auto;
<p>This application follows the MVC (Model-View-Controller) architectural pattern:</p> }
<div class="row"> </style>
<div class="col-md-4">
<div class="card mb-3"> <div class="row g-3 mb-4">
<div class="card-header bg-primary text-white"> <!-- Left Column (3 items) -->
Models <div class="col-md-6">
<!-- Card 1: Frictionless Collaboration -->
<div class="card shadow mb-3 border-primary">
<div class="card-header py-2 bg-primary bg-opacity-10 border-primary">
<h6 class="mb-0 text-primary"><i class="bi bi-people-fill me-2"></i>Frictionless Collaboration</h6>
</div> </div>
<div class="card-body"> <div class="card-body p-2 compact-card">
<p>Data structures and business logic</p> <p class="card-text small">Direct communication and transactions between individuals and organizations, making processes efficient and cost-effective.</p>
</div>
</div>
<!-- Card 2: Frictionless Banking -->
<div class="card shadow mb-3 border-success">
<div class="card-header py-2 bg-success bg-opacity-10 border-success">
<h6 class="mb-0 text-success"><i class="bi bi-currency-exchange me-2"></i>Frictionless Banking</h6>
</div>
<div class="card-body p-2 compact-card">
<p class="card-text small">Simplified financial transactions without the complications and fees of traditional banking systems.</p>
</div>
</div>
<!-- Card 3: Tax Efficiency -->
<div class="card shadow mb-3 border-info">
<div class="card-header py-2 bg-info bg-opacity-10 border-info">
<h6 class="mb-0 text-info"><i class="bi bi-graph-up-arrow me-2"></i>Tax Efficiency</h6>
</div>
<div class="card-body p-2 compact-card">
<p class="card-text small">Lower taxes making business operations more profitable and competitive in the global market.</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4">
<div class="card mb-3"> <!-- Right Column (2 items) -->
<div class="card-header bg-success text-white"> <div class="col-md-6">
Views <!-- Card 4: Global Ecommerce -->
<div class="card shadow mb-3 border-warning">
<div class="card-header py-2 bg-warning bg-opacity-10 border-warning">
<h6 class="mb-0 text-warning"><i class="bi bi-globe me-2"></i>Global Ecommerce</h6>
</div> </div>
<div class="card-body"> <div class="card-body p-2 compact-card">
<p>Tera templates for rendering HTML</p> <p class="card-text small">Easily expand your business globally with streamlined operations and tools to reach customers worldwide.</p>
</div> </div>
</div> </div>
</div>
<div class="col-md-4"> <!-- Card 5: Clear Regulations -->
<div class="card mb-3"> <div class="card shadow mb-3 border-danger">
<div class="card-header bg-info text-white"> <div class="card-header py-2 bg-danger bg-opacity-10 border-danger">
Controllers <h6 class="mb-0 text-danger"><i class="bi bi-shield-check me-2"></i>Clear Regulations</h6>
</div> </div>
<div class="card-body"> <div class="card-body p-2 compact-card">
<p>Request handlers and application logic</p> <p class="card-text small">Clear regulations and efficient dispute resolution mechanisms providing a stable business environment.</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<a href="/about" class="btn btn-primary">Learn More</a>
<div class="text-center">
<a href="/about" class="btn btn-primary btn-lg">Learn More</a>
</div>
</div> </div>
</div> </div>
</div> </div>

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

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Support Tickets - Actix MVC App{% endblock %} {% block title %}Support Tickets{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}My Tickets - Actix MVC App{% endblock %} {% block title %}My Tickets{% endblock %}
{% block content %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}New Support Ticket - Actix MVC App{% endblock %} {% block title %}New Support Ticket{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">

View File

@@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Ticket #{{ ticket.id | truncate(length=8) }} - Actix MVC App{% endblock %} {% block title %}Ticket #{{ ticket.id | truncate(length=8) }}{% endblock %}
{% block content %} {% block content %}
<div class="container" up-main> <div class="container-fluid" up-main>
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1>Ticket #{{ ticket.id | truncate(length=8) }}</h1> <h1>Ticket #{{ ticket.id | truncate(length=8) }}</h1>
<div> <div>

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.

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