Compare commits
31 Commits
36812e4178
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c2276de79 | ||
|
|
123dfc606c | ||
| 795c04fc5a | |||
|
|
2cfec627bf | ||
|
|
83dde53555 | ||
|
|
2fd74defab | ||
|
|
9468595395 | ||
|
|
2760f00a30 | ||
|
|
a7c0772d9b | ||
|
|
54762cb63f | ||
|
|
bafb63e0b1 | ||
|
|
c05803ff58 | ||
|
|
6b7b2542ab | ||
| 457f3c8268 | |||
|
|
19f8700b78 | ||
|
|
c22d6c953e | ||
|
|
9445dea629 | ||
|
|
b56f1cbc30 | ||
|
|
6060831f61 | ||
| 34594b95fa | |||
| 15b05cb599 | |||
| b6dd04a6aa | |||
| 310a5d956f | |||
| af4f09a67b | |||
| 093aff3851 | |||
| 4a87392194 | |||
|
|
951af7dec7 | ||
|
|
36d605829f | ||
|
|
6ed6737c7e | ||
| 361dff22c5 | |||
| 4a8f65e155 |
193
actix_mvc_app/Cargo.lock
generated
193
actix_mvc_app/Cargo.lock
generated
@@ -107,6 +107,45 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-multipart"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d974dd6c4f78d102d057c672dcf6faa618fafa9df91d44f9c466688fc1275a3a"
|
||||
dependencies = [
|
||||
"actix-multipart-derive",
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"bytes",
|
||||
"derive_more 0.99.19",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"httparse",
|
||||
"local-waker",
|
||||
"log",
|
||||
"memchr",
|
||||
"mime",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_plain",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-multipart-derive"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"parse-size",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.3"
|
||||
@@ -247,6 +286,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-identity",
|
||||
"actix-multipart",
|
||||
"actix-session",
|
||||
"actix-web",
|
||||
"bcrypt",
|
||||
@@ -255,14 +295,17 @@ dependencies = [
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"num_cpus",
|
||||
"pulldown-cmark",
|
||||
"redis",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tera",
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -821,6 +864,41 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
@@ -945,6 +1023,22 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.1"
|
||||
@@ -1075,6 +1169,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
@@ -1386,6 +1489,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
@@ -1553,6 +1662,12 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.5"
|
||||
@@ -1749,6 +1864,12 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-size"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.1"
|
||||
@@ -1931,6 +2052,25 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark-escape"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -2122,6 +2262,19 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
@@ -2187,6 +2340,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_plain"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
@@ -2326,6 +2488,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -2354,6 +2522,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tera"
|
||||
version = "1.20.0"
|
||||
@@ -2622,6 +2803,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -2655,6 +2842,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
|
||||
@@ -4,6 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-multipart = "0.6.1"
|
||||
futures-util = "0.3.30"
|
||||
actix-web = "4.5.1"
|
||||
actix-files = "0.6.5"
|
||||
tera = "1.19.1"
|
||||
@@ -23,3 +25,5 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] }
|
||||
lazy_static = "1.4.0"
|
||||
redis = { version = "0.23.0", features = ["tokio-comp"] }
|
||||
jsonwebtoken = "8.3.0"
|
||||
pulldown-cmark = "0.13.0"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,8 +42,8 @@ actix_mvc_app/
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/yourusername/actix_mvc_app.git
|
||||
cd actix_mvc_app
|
||||
git clone https://github.com/yourusername/zanzibar-digital-freezone.git
|
||||
cd zanzibar-digital-freezone
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
|
||||
- login
|
||||
- change profile (email(s), linkedin, websites, telnr's, ...)
|
||||
- KYC
|
||||
- wallet
|
||||
- with EUR/CHF/USD, ability to topup wallet... (stripe, make sure to describe well)
|
||||
- with real world digital assets (RWDA)
|
||||
- the RWDA can be transfered to someone else if allowed
|
||||
- tickets (see own tickets)
|
||||
- a ticket can have workflow attached to it (as set by rhai script)
|
||||
- on a workflow step needs to be clear for user if they need to do something
|
||||
- has type, subject, priority, level, comments (user can add comment)
|
||||
- if new comment message is sent to inbox as well, if new action needed as well
|
||||
- inbox
|
||||
- info we can send to the user, user can reply
|
||||
- has labels
|
||||
- actions
|
||||
- if something to do for user, is part of the workflow
|
||||
- RWDA'S
|
||||
- overview (in tiles like blogs) of the RWDA's, has tags user can filter
|
||||
- if user clicks on one then goes to mini site (like ebook), is shown in app
|
||||
- on RWDA we see nr of RWDA (marketcap, ... and other core financials, ...)
|
||||
- use can select RWDA, and buy into it, if not enough cash will be asked to put more cash in
|
||||
- contracts
|
||||
- as markdown
|
||||
- user can sign
|
||||
- see who signed
|
||||
|
||||
# user flows
|
||||
|
||||
## registration
|
||||
|
||||
- login, user choses secret (done by means of the webassembly component)
|
||||
- verification level, user can do KYC
|
||||
|
||||
|
||||
# rwda
|
||||
|
||||
- name
|
||||
- description
|
||||
- link to website
|
||||
- nr of shares
|
||||
- share value
|
||||
- vesting period, lockin period
|
||||
- symbol
|
||||
|
||||
|
||||
|
||||
# Dynex
|
||||
|
||||
- meeting with
|
||||
|
||||
|
||||
|
||||
|
||||
3
actix_mvc_app/src/content/contract-003/1-purpose.md
Normal file
3
actix_mvc_app/src/content/contract-003/1-purpose.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
3
actix_mvc_app/src/content/contract-003/4-governance.md
Normal file
3
actix_mvc_app/src/content/contract-003/4-governance.md
Normal 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.
|
||||
3
actix_mvc_app/src/content/contract-003/appendix-a.md
Normal file
3
actix_mvc_app/src/content/contract-003/appendix-a.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Appendix A: Properties
|
||||
|
||||
List of properties to be tokenized.
|
||||
3
actix_mvc_app/src/content/contract-003/appendix-b.md
Normal file
3
actix_mvc_app/src/content/contract-003/appendix-b.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Appendix B: Specifications
|
||||
|
||||
Technical specifications for tokenization.
|
||||
3
actix_mvc_app/src/content/contract-003/appendix-c.md
Normal file
3
actix_mvc_app/src/content/contract-003/appendix-c.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Appendix C: Revenue Formula
|
||||
|
||||
Formula for revenue distribution.
|
||||
3
actix_mvc_app/src/content/contract-003/appendix-d.md
Normal file
3
actix_mvc_app/src/content/contract-003/appendix-d.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Appendix D: Governance Framework
|
||||
|
||||
Governance framework for tokenized properties.
|
||||
3
actix_mvc_app/src/content/contract-003/cover.md
Normal file
3
actix_mvc_app/src/content/contract-003/cover.md
Normal 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").
|
||||
776
actix_mvc_app/src/controllers/asset.rs
Normal file
776
actix_mvc_app/src/controllers/asset.rs
Normal file
@@ -0,0 +1,776 @@
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use tera::{Context, Tera};
|
||||
use chrono::{Utc, Duration};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::asset::{Asset, AssetType, AssetStatus, BlockchainInfo, ValuationPoint, AssetTransaction, AssetStatistics};
|
||||
use crate::utils::render_template;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssetForm {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub asset_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ValuationForm {
|
||||
pub value: f64,
|
||||
pub currency: String,
|
||||
pub source: String,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TransactionForm {
|
||||
pub transaction_type: String,
|
||||
pub from_address: Option<String>,
|
||||
pub to_address: Option<String>,
|
||||
pub amount: Option<f64>,
|
||||
pub currency: Option<String>,
|
||||
pub transaction_hash: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AssetController;
|
||||
|
||||
impl AssetController {
|
||||
// Display the assets dashboard
|
||||
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
|
||||
let mut context = Context::new();
|
||||
|
||||
println!("DEBUG: Starting assets dashboard rendering");
|
||||
|
||||
let assets = Self::get_mock_assets();
|
||||
println!("DEBUG: Generated {} mock assets", assets.len());
|
||||
|
||||
let stats = AssetStatistics::new(&assets);
|
||||
println!("DEBUG: Generated asset statistics: {:?}", stats);
|
||||
|
||||
// Add active_page for navigation highlighting
|
||||
context.insert("active_page", &"assets");
|
||||
|
||||
// Add stats
|
||||
context.insert("stats", &serde_json::to_value(stats).unwrap());
|
||||
println!("DEBUG: Added stats to context");
|
||||
|
||||
// Add recent assets
|
||||
let recent_assets: Vec<serde_json::Map<String, serde_json::Value>> = assets
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|a| Self::asset_to_json(a))
|
||||
.collect();
|
||||
|
||||
context.insert("recent_assets", &recent_assets);
|
||||
|
||||
// Add assets by type
|
||||
let asset_types = vec![
|
||||
AssetType::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
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use actix_web::{web, HttpResponse, Responder, Result, http::header, cookie::Cook
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use crate::models::user::{User, LoginCredentials, RegistrationData, UserRole};
|
||||
use crate::utils::render_template;
|
||||
use jsonwebtoken::{encode, decode, Header, Algorithm, Validation, EncodingKey, DecodingKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{Utc, Duration};
|
||||
@@ -91,13 +92,7 @@ impl AuthController {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "login");
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "auth/login.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles user login
|
||||
@@ -146,13 +141,7 @@ impl AuthController {
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("active_page", "register");
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "auth/register.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles user registration
|
||||
|
||||
@@ -6,7 +6,7 @@ use tera::Tera;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::models::{CalendarEvent, CalendarViewMode};
|
||||
use crate::utils::RedisCalendarService;
|
||||
use crate::utils::{RedisCalendarService, render_template};
|
||||
|
||||
/// Controller for handling calendar-related routes
|
||||
pub struct CalendarController;
|
||||
@@ -215,13 +215,7 @@ impl CalendarController {
|
||||
},
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "calendar/index.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles the new event page route
|
||||
@@ -234,13 +228,7 @@ impl CalendarController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "calendar/new_event.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles the create event route
|
||||
@@ -298,13 +286,9 @@ impl CalendarController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("calendar/new_event.html", &ctx)
|
||||
.map_err(|e| {
|
||||
eprintln!("Template rendering error: {}", e);
|
||||
actix_web::error::ErrorInternalServerError("Template rendering error")
|
||||
})?;
|
||||
let result = render_template(&tmpl, "calendar/new_event.html", &ctx)?;
|
||||
|
||||
Ok(HttpResponse::InternalServerError().content_type("text/html").body(rendered))
|
||||
Ok(HttpResponse::InternalServerError().content_type("text/html").body(result.into_body()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
245
actix_mvc_app/src/controllers/company.rs
Normal file
245
actix_mvc_app/src/controllers/company.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
742
actix_mvc_app/src/controllers/contract.rs
Normal file
742
actix_mvc_app/src/controllers/contract.rs
Normal 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
|
||||
}
|
||||
}
|
||||
368
actix_mvc_app/src/controllers/defi.rs
Normal file
368
actix_mvc_app/src/controllers/defi.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
633
actix_mvc_app/src/controllers/flow.rs
Normal file
633
actix_mvc_app/src/controllers/flow.rs
Normal 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,
|
||||
}
|
||||
568
actix_mvc_app/src/controllers/governance.rs
Normal file
568
actix_mvc_app/src/controllers/governance.rs
Normal 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,
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use actix_web::{web, HttpResponse, Responder, Result};
|
||||
use actix_web::{web, Responder, Result};
|
||||
use actix_session::Session;
|
||||
use tera::Tera;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::utils::render_template;
|
||||
|
||||
/// Controller for handling home-related routes
|
||||
pub struct HomeController;
|
||||
|
||||
@@ -24,13 +26,7 @@ impl HomeController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "editor.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles the home page route
|
||||
@@ -43,13 +39,7 @@ impl HomeController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "index.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles the about page route
|
||||
@@ -62,13 +52,7 @@ impl HomeController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "about.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles the contact page route
|
||||
@@ -81,13 +65,7 @@ impl HomeController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "contact.html", &ctx)
|
||||
}
|
||||
|
||||
/// Handles form submissions from the contact page
|
||||
@@ -112,13 +90,7 @@ impl HomeController {
|
||||
ctx.insert("user", &user);
|
||||
}
|
||||
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "contact.html", &ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
576
actix_mvc_app/src/controllers/marketplace.rs
Normal file
576
actix_mvc_app/src/controllers/marketplace.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,12 @@ pub mod home;
|
||||
pub mod auth;
|
||||
pub mod ticket;
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ use tera::Tera;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use crate::models::{User, Ticket, TicketComment, TicketStatus, TicketPriority};
|
||||
use crate::utils::render_template;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -131,13 +132,7 @@ impl TicketController {
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "tickets/list.html", &ctx)
|
||||
}
|
||||
|
||||
/// Shows the form for creating a new ticket
|
||||
@@ -172,13 +167,7 @@ impl TicketController {
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "tickets/new.html", &ctx)
|
||||
}
|
||||
|
||||
/// Creates a new ticket
|
||||
@@ -285,13 +274,7 @@ impl TicketController {
|
||||
]);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "tickets/show.html", &ctx)
|
||||
}
|
||||
|
||||
/// Adds a comment to a ticket
|
||||
@@ -443,12 +426,6 @@ impl TicketController {
|
||||
ctx.insert("my_tickets", &true);
|
||||
|
||||
// Render the template
|
||||
let rendered = tmpl.render("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))
|
||||
render_template(&tmpl, "tickets/list.html", &ctx)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ mod utils;
|
||||
// Import middleware components
|
||||
use middleware::{RequestTimer, SecurityHeaders, JwtAuth};
|
||||
use utils::redis_service;
|
||||
use models::initialize_mock_data;
|
||||
|
||||
// Initialize lazy_static for in-memory storage
|
||||
extern crate lazy_static;
|
||||
@@ -72,6 +73,10 @@ async fn main() -> io::Result<()> {
|
||||
log::info!("Redis client initialized successfully");
|
||||
}
|
||||
|
||||
// Initialize mock data for DeFi operations
|
||||
initialize_mock_data();
|
||||
log::info!("DeFi mock data initialized successfully");
|
||||
|
||||
log::info!("Starting server at http://{}", bind_address);
|
||||
|
||||
// Create and configure the HTTP server
|
||||
|
||||
282
actix_mvc_app/src/models/asset.rs
Normal file
282
actix_mvc_app/src/models/asset.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Asset types representing different categories of digital assets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AssetType {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
314
actix_mvc_app/src/models/contract.rs
Normal file
314
actix_mvc_app/src/models/contract.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
206
actix_mvc_app/src/models/defi.rs
Normal file
206
actix_mvc_app/src/models/defi.rs
Normal 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);
|
||||
}
|
||||
387
actix_mvc_app/src/models/flow.rs
Normal file
387
actix_mvc_app/src/models/flow.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
248
actix_mvc_app/src/models/governance.rs
Normal file
248
actix_mvc_app/src/models/governance.rs
Normal 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
|
||||
}
|
||||
}
|
||||
295
actix_mvc_app/src/models/marketplace.rs
Normal file
295
actix_mvc_app/src/models/marketplace.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,16 @@
|
||||
pub mod user;
|
||||
pub mod ticket;
|
||||
pub mod calendar;
|
||||
pub mod governance;
|
||||
pub mod flow;
|
||||
pub mod contract;
|
||||
pub mod asset;
|
||||
pub mod marketplace;
|
||||
pub mod defi;
|
||||
|
||||
// Re-export models for easier imports
|
||||
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 marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics};
|
||||
pub use defi::{DefiPosition, DefiPositionType, DefiPositionStatus, ProvidingPosition, ReceivingPosition, DEFI_DB, initialize_mock_data};
|
||||
|
||||
@@ -145,8 +145,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_new_user() {
|
||||
let user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
assert_eq!(user.name, "John Doe");
|
||||
let user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
|
||||
assert_eq!(user.name, "Robert Callingham");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
assert!(!user.is_admin());
|
||||
}
|
||||
@@ -161,13 +161,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_update_user() {
|
||||
let mut user = User::new("John Doe".to_string(), "john@example.com".to_string());
|
||||
user.update(Some("Jane Doe".to_string()), None);
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
let mut user = User::new("Robert Callingham".to_string(), "john@example.com".to_string());
|
||||
user.update(Some("Mary Hewell".to_string()), None);
|
||||
assert_eq!(user.name, "Mary Hewell");
|
||||
assert_eq!(user.email, "john@example.com");
|
||||
|
||||
user.update(None, Some("jane@example.com".to_string()));
|
||||
assert_eq!(user.name, "Jane Doe");
|
||||
assert_eq!(user.name, "Mary Hewell");
|
||||
assert_eq!(user.email, "jane@example.com");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,13 @@ use crate::controllers::home::HomeController;
|
||||
use crate::controllers::auth::AuthController;
|
||||
use crate::controllers::ticket::TicketController;
|
||||
use crate::controllers::calendar::CalendarController;
|
||||
use crate::controllers::governance::GovernanceController;
|
||||
use crate::controllers::flow::FlowController;
|
||||
use crate::controllers::contract::ContractController;
|
||||
use crate::controllers::asset::AssetController;
|
||||
use crate::controllers::marketplace::MarketplaceController;
|
||||
use crate::controllers::defi::DefiController;
|
||||
use crate::controllers::company::CompanyController;
|
||||
use crate::middleware::JwtAuth;
|
||||
use crate::SESSION_KEY;
|
||||
|
||||
@@ -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", web::post().to(CalendarController::create_event))
|
||||
.route("/calendar/events/{id}/delete", web::post().to(CalendarController::delete_event))
|
||||
|
||||
// Governance routes
|
||||
.route("/governance", web::get().to(GovernanceController::index))
|
||||
.route("/governance/proposals", web::get().to(GovernanceController::proposals))
|
||||
.route("/governance/proposals/{id}", web::get().to(GovernanceController::proposal_detail))
|
||||
.route("/governance/proposals/{id}/vote", web::post().to(GovernanceController::submit_vote))
|
||||
.route("/governance/create", web::get().to(GovernanceController::create_proposal_form))
|
||||
.route("/governance/create", web::post().to(GovernanceController::submit_proposal))
|
||||
.route("/governance/my-votes", web::get().to(GovernanceController::my_votes))
|
||||
|
||||
// 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
|
||||
|
||||
173
actix_mvc_app/src/static/js/company.js
Normal file
173
actix_mvc_app/src/static/js/company.js
Normal 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');
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
use actix_web::{error, Error, HttpResponse};
|
||||
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
|
||||
pub mod redis_service;
|
||||
@@ -7,6 +9,22 @@ pub mod redis_service;
|
||||
// Re-export for easier imports
|
||||
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
|
||||
pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||
tera.register_function("now", NowFunction);
|
||||
@@ -18,7 +36,7 @@ pub fn register_tera_functions(tera: &mut tera::Tera) {
|
||||
pub struct 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") {
|
||||
Some(val) => match val.as_str() {
|
||||
Some(s) => s,
|
||||
@@ -43,7 +61,7 @@ impl Function for NowFunction {
|
||||
pub struct 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") {
|
||||
Some(val) => match val.as_i64() {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About - Actix MVC App{% endblock %}
|
||||
{% block title %}About - Zanzibar Digital Freezone{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">About Actix MVC App</h1>
|
||||
<p class="card-text">This is a sample application demonstrating how to build a web application using Rust with an MVC architecture.</p>
|
||||
<h1 class="card-title">About Zanzibar Digital Freezone</h1>
|
||||
<p class="card-text">Convenience, Safety and Privacy</p>
|
||||
|
||||
<h2 class="mt-4">Technology Stack</h2>
|
||||
<div class="row">
|
||||
|
||||
271
actix_mvc_app/src/views/assets/create.html
Normal file
271
actix_mvc_app/src/views/assets/create.html
Normal file
@@ -0,0 +1,271 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create New Digital Asset{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Create New Digital Asset</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">Create New Asset</li>
|
||||
</ol>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-plus-circle me-1"></i>
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="createAssetForm" method="post" action="/assets/create">
|
||||
<!-- Basic Information -->
|
||||
<div class="mb-4">
|
||||
<h5>Basic Information</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="asset_type" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="asset_type" name="asset_type" required>
|
||||
{% for type_value, type_label in asset_types %}
|
||||
<option value="{{ type_value }}">{{ type_label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="image_url" class="form-label">Image URL (optional)</label>
|
||||
<input type="url" class="form-control" id="image_url" name="image_url">
|
||||
<div class="form-text">URL to an image representing this asset</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="external_url" class="form-label">External URL (optional)</label>
|
||||
<input type="url" class="form-control" id="external_url" name="external_url">
|
||||
<div class="form-text">URL to an external resource for this asset</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blockchain Information -->
|
||||
<div class="mb-4">
|
||||
<h5>Blockchain Information (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_blockchain_info" name="has_blockchain_info">
|
||||
<label class="form-check-label" for="has_blockchain_info">
|
||||
This asset has blockchain information
|
||||
</label>
|
||||
</div>
|
||||
<div id="blockchainInfoSection" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="blockchain" class="form-label">Blockchain</label>
|
||||
<input type="text" class="form-control" id="blockchain" name="blockchain">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="token_id" class="form-label">Token ID</label>
|
||||
<input type="text" class="form-control" id="token_id" name="token_id">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="contract_address" class="form-label">Contract Address</label>
|
||||
<input type="text" class="form-control" id="contract_address" name="contract_address">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="owner_address" class="form-label">Owner Address</label>
|
||||
<input type="text" class="form-control" id="owner_address" name="owner_address">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="transaction_hash" class="form-label">Transaction Hash (optional)</label>
|
||||
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="block_number" class="form-label">Block Number (optional)</label>
|
||||
<input type="number" class="form-control" id="block_number" name="block_number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Initial Valuation -->
|
||||
<div class="mb-4">
|
||||
<h5>Initial Valuation (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_valuation" name="has_valuation">
|
||||
<label class="form-check-label" for="has_valuation">
|
||||
Add an initial valuation for this asset
|
||||
</label>
|
||||
</div>
|
||||
<div id="valuationSection" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD">
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="valuation_notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="valuation_notes" name="valuation_notes" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="mb-4">
|
||||
<h5>Additional Metadata (optional)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="has_metadata" name="has_metadata">
|
||||
<label class="form-check-label" for="has_metadata">
|
||||
Add additional metadata for this asset
|
||||
</label>
|
||||
</div>
|
||||
<div id="metadataSection" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="metadata" class="form-label">Metadata (JSON format)</label>
|
||||
<textarea class="form-control" id="metadata" name="metadata" rows="5"></textarea>
|
||||
<div class="form-text">Enter additional metadata in JSON format</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="/assets" class="btn btn-secondary me-md-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Create Asset</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Toggle blockchain info section
|
||||
const hasBlockchainInfo = document.getElementById('has_blockchain_info');
|
||||
const blockchainInfoSection = document.getElementById('blockchainInfoSection');
|
||||
|
||||
hasBlockchainInfo.addEventListener('change', function() {
|
||||
blockchainInfoSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Toggle valuation section
|
||||
const hasValuation = document.getElementById('has_valuation');
|
||||
const valuationSection = document.getElementById('valuationSection');
|
||||
|
||||
hasValuation.addEventListener('change', function() {
|
||||
valuationSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Toggle metadata section
|
||||
const hasMetadata = document.getElementById('has_metadata');
|
||||
const metadataSection = document.getElementById('metadataSection');
|
||||
|
||||
hasMetadata.addEventListener('change', function() {
|
||||
metadataSection.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
|
||||
// Form validation
|
||||
const form = document.getElementById('createAssetForm');
|
||||
form.addEventListener('submit', function(event) {
|
||||
let isValid = true;
|
||||
|
||||
// Validate required fields
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const description = document.getElementById('description').value.trim();
|
||||
|
||||
if (!name) {
|
||||
isValid = false;
|
||||
document.getElementById('name').classList.add('is-invalid');
|
||||
} else {
|
||||
document.getElementById('name').classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
isValid = false;
|
||||
document.getElementById('description').classList.add('is-invalid');
|
||||
} else {
|
||||
document.getElementById('description').classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate blockchain info if checked
|
||||
if (hasBlockchainInfo.checked) {
|
||||
const blockchain = document.getElementById('blockchain').value.trim();
|
||||
const tokenId = document.getElementById('token_id').value.trim();
|
||||
const contractAddress = document.getElementById('contract_address').value.trim();
|
||||
const ownerAddress = document.getElementById('owner_address').value.trim();
|
||||
|
||||
if (!blockchain || !tokenId || !contractAddress || !ownerAddress) {
|
||||
isValid = false;
|
||||
if (!blockchain) document.getElementById('blockchain').classList.add('is-invalid');
|
||||
if (!tokenId) document.getElementById('token_id').classList.add('is-invalid');
|
||||
if (!contractAddress) document.getElementById('contract_address').classList.add('is-invalid');
|
||||
if (!ownerAddress) document.getElementById('owner_address').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate valuation if checked
|
||||
if (hasValuation.checked) {
|
||||
const value = document.getElementById('value').value.trim();
|
||||
const currency = document.getElementById('currency').value.trim();
|
||||
const source = document.getElementById('source').value.trim();
|
||||
|
||||
if (!value || !currency || !source) {
|
||||
isValid = false;
|
||||
if (!value) document.getElementById('value').classList.add('is-invalid');
|
||||
if (!currency) document.getElementById('currency').classList.add('is-invalid');
|
||||
if (!source) document.getElementById('source').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate metadata if checked
|
||||
if (hasMetadata.checked) {
|
||||
const metadata = document.getElementById('metadata').value.trim();
|
||||
|
||||
if (metadata) {
|
||||
try {
|
||||
JSON.parse(metadata);
|
||||
document.getElementById('metadata').classList.remove('is-invalid');
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
document.getElementById('metadata').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
alert('Please fix the errors in the form before submitting.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
556
actix_mvc_app/src/views/assets/detail.html
Normal file
556
actix_mvc_app/src/views/assets/detail.html
Normal file
@@ -0,0 +1,556 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Asset Details - {{ asset.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Asset Details</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">{{ asset.name }}</li>
|
||||
</ol>
|
||||
|
||||
<!-- Asset Overview -->
|
||||
<div class="row">
|
||||
<div class="col-xl-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Asset Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-4">
|
||||
{% if asset.image_url %}
|
||||
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="img-fluid asset-image mb-3">
|
||||
{% else %}
|
||||
<div class="asset-placeholder mb-3">
|
||||
<i class="fas fa-cube fa-5x"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h3>{{ asset.name }}</h3>
|
||||
<div>
|
||||
{% if asset.status == "Active" %}
|
||||
<span class="badge bg-success">{{ asset.status }}</span>
|
||||
{% elif asset.status == "For Sale" %}
|
||||
<span class="badge bg-warning">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Locked" %}
|
||||
<span class="badge bg-secondary">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Transferred" %}
|
||||
<span class="badge bg-info">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Archived" %}
|
||||
<span class="badge bg-danger">{{ asset.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ asset.status }}</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-primary">{{ asset.asset_type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Description</h5>
|
||||
<p>{{ asset.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Current Valuation</h5>
|
||||
{% if asset.current_valuation %}
|
||||
<h3 class="text-primary">{{ asset.valuation_currency }}{{ asset.current_valuation }}</h3>
|
||||
<small class="text-muted">Last updated: {{ asset.valuation_date }}</small>
|
||||
{% else %}
|
||||
<p>No valuation available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Owner Information</h5>
|
||||
<p><strong>Owner:</strong> {{ asset.owner_name }}</p>
|
||||
<p><strong>Owner ID:</strong> {{ asset.owner_id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h5>Dates</h5>
|
||||
<p><strong>Created:</strong> {{ asset.created_at }}</p>
|
||||
<p><strong>Last Updated:</strong> {{ asset.updated_at }}</p>
|
||||
</div>
|
||||
|
||||
{% if asset.external_url %}
|
||||
<div class="mb-3">
|
||||
<a href="{{ asset.external_url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt me-1"></i> View External Resource
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#valuationModal">
|
||||
<i class="fas fa-dollar-sign me-1"></i> Add Valuation
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" data-bs-toggle="modal" data-bs-target="#transactionModal">
|
||||
<i class="fas fa-exchange-alt me-1"></i> Record Transaction
|
||||
</button>
|
||||
<button class="btn btn-warning" type="button" data-bs-toggle="modal" data-bs-target="#statusModal">
|
||||
<i class="fas fa-edit me-1"></i> Change Status
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-8">
|
||||
<!-- Blockchain Information -->
|
||||
{% if asset.blockchain_info %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-link me-1"></i>
|
||||
Blockchain Information
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Blockchain:</strong> {{ asset.blockchain_info.blockchain }}</p>
|
||||
<p><strong>Token ID:</strong> {{ asset.blockchain_info.token_id }}</p>
|
||||
<p><strong>Contract Address:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.contract_address }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Owner Address:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.owner_address }}</code>
|
||||
</p>
|
||||
{% if asset.blockchain_info.transaction_hash %}
|
||||
<p><strong>Transaction Hash:</strong>
|
||||
<code class="blockchain-address">{{ asset.blockchain_info.transaction_hash }}</code>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if asset.blockchain_info.block_number %}
|
||||
<p><strong>Block Number:</strong> {{ asset.blockchain_info.block_number }}</p>
|
||||
{% endif %}
|
||||
{% if asset.blockchain_info.timestamp %}
|
||||
<p><strong>Timestamp:</strong> {{ asset.blockchain_info.timestamp }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Valuation History Chart -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-line me-1"></i>
|
||||
Valuation History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if valuation_history and valuation_history|length > 0 %}
|
||||
<canvas id="valuationChart" width="100%" height="40"></canvas>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No valuation history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation History Table -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-history me-1"></i>
|
||||
Valuation History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if asset.valuation_history and asset.valuation_history|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Value</th>
|
||||
<th>Currency</th>
|
||||
<th>Source</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for valuation in asset.valuation_history %}
|
||||
<tr>
|
||||
<td>{{ valuation.date }}</td>
|
||||
<td>{{ valuation.value }}</td>
|
||||
<td>{{ valuation.currency }}</td>
|
||||
<td>{{ valuation.source }}</td>
|
||||
<td>{% if valuation.notes %}{{ valuation.notes }}{% else %}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No valuation history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction History -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-exchange-alt me-1"></i>
|
||||
Transaction History
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if asset.transaction_history and asset.transaction_history|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Amount</th>
|
||||
<th>Transaction Hash</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transaction in asset.transaction_history %}
|
||||
<tr>
|
||||
<td>{{ transaction.date }}</td>
|
||||
<td>{{ transaction.transaction_type }}</td>
|
||||
<td>
|
||||
{% if transaction.from_address %}
|
||||
<code class="blockchain-address-small">{{ transaction.from_address }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.to_address %}
|
||||
<code class="blockchain-address-small">{{ transaction.to_address }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.amount %}
|
||||
{{ transaction.currency }}{{ transaction.amount }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if transaction.transaction_hash %}
|
||||
<code class="blockchain-address-small">{{ transaction.transaction_hash }}</code>
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if transaction.notes %}{{ transaction.notes }}{% else %}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
No transaction history available for this asset.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation Modal -->
|
||||
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="valuationForm" method="post" action="/assets/{{ asset.id }}/valuation">
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Modal -->
|
||||
<div class="modal fade" id="transactionModal" tabindex="-1" aria-labelledby="transactionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="transactionModalLabel">Record Transaction</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="transactionForm" method="post" action="/assets/{{ asset.id }}/transaction">
|
||||
<div class="mb-3">
|
||||
<label for="transaction_type" class="form-label">Transaction Type</label>
|
||||
<select class="form-select" id="transaction_type" name="transaction_type" required>
|
||||
<option value="Purchase">Purchase</option>
|
||||
<option value="Sale">Sale</option>
|
||||
<option value="Transfer">Transfer</option>
|
||||
<option value="Mint">Mint</option>
|
||||
<option value="Burn">Burn</option>
|
||||
<option value="Licensing">Licensing</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="from_address" class="form-label">From Address</label>
|
||||
<input type="text" class="form-control" id="from_address" name="from_address">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="to_address" class="form-label">To Address</label>
|
||||
<input type="text" class="form-control" id="to_address" name="to_address">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Amount</label>
|
||||
<input type="number" class="form-control" id="amount" name="amount" step="0.01">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="transaction_currency" name="currency" value="USD">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_hash" class="form-label">Transaction Hash</label>
|
||||
<input type="text" class="form-control" id="transaction_hash" name="transaction_hash">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="transaction_notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="transaction_notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveTransactionBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm" method="post" action="/assets/{{ asset.id }}/status/">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Status</label>
|
||||
<select class="form-select" id="newStatus" name="status">
|
||||
<option value="Active" {% if asset.status == "Active" %}selected{% endif %}>Active</option>
|
||||
<option value="Locked" {% if asset.status == "Locked" %}selected{% endif %}>Locked</option>
|
||||
<option value="ForSale" {% if asset.status == "For Sale" %}selected{% endif %}>For Sale</option>
|
||||
<option value="Transferred" {% if asset.status == "Transferred" %}selected{% endif %}>Transferred</option>
|
||||
<option value="Archived" {% if asset.status == "Archived" %}selected{% endif %}>Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Valuation History Chart
|
||||
{% if valuation_history and valuation_history|length > 0 %}
|
||||
const ctx = document.getElementById('valuationChart');
|
||||
|
||||
const dates = [
|
||||
{% for point in valuation_history %}
|
||||
"{{ point.date }}"{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const values = [
|
||||
{% for point in valuation_history %}
|
||||
{{ point.value }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dates,
|
||||
datasets: [{
|
||||
label: 'Valuation ({{ valuation_history[0].currency }})',
|
||||
data: values,
|
||||
lineTension: 0.3,
|
||||
backgroundColor: "rgba(78, 115, 223, 0.05)",
|
||||
borderColor: "rgba(78, 115, 223, 1)",
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: "rgba(78, 115, 223, 1)",
|
||||
pointBorderColor: "rgba(78, 115, 223, 1)",
|
||||
pointHoverRadius: 5,
|
||||
pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
|
||||
pointHoverBorderColor: "rgba(78, 115, 223, 1)",
|
||||
pointHitRadius: 10,
|
||||
pointBorderWidth: 2,
|
||||
fill: true
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 7
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
maxTicksLimit: 5,
|
||||
padding: 10,
|
||||
callback: function(value, index, values) {
|
||||
return '{{ valuation_history[0].currency }}' + value;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: "rgb(234, 236, 244)",
|
||||
zeroLineColor: "rgb(234, 236, 244)",
|
||||
drawBorder: false,
|
||||
borderDash: [2],
|
||||
zeroLineBorderDash: [2]
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "rgb(255,255,255)",
|
||||
bodyFontColor: "#858796",
|
||||
titleMarginBottom: 10,
|
||||
titleFontColor: '#6e707e',
|
||||
titleFontSize: 14,
|
||||
borderColor: '#dddfeb',
|
||||
borderWidth: 1,
|
||||
xPadding: 15,
|
||||
yPadding: 15,
|
||||
displayColors: false,
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
caretPadding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
var label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += '{{ valuation_history[0].currency }}' + context.parsed.y;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
// Form submission handlers
|
||||
const saveValuationBtn = document.getElementById('saveValuationBtn');
|
||||
if (saveValuationBtn) {
|
||||
saveValuationBtn.addEventListener('click', function() {
|
||||
document.getElementById('valuationForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
const saveTransactionBtn = document.getElementById('saveTransactionBtn');
|
||||
if (saveTransactionBtn) {
|
||||
saveTransactionBtn.addEventListener('click', function() {
|
||||
document.getElementById('transactionForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
const saveStatusBtn = document.getElementById('saveStatusBtn');
|
||||
if (saveStatusBtn) {
|
||||
saveStatusBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('statusForm');
|
||||
const newStatus = document.getElementById('newStatus').value;
|
||||
form.action = form.action + newStatus;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.asset-image {
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.asset-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.blockchain-address {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.blockchain-address-small {
|
||||
display: inline-block;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
197
actix_mvc_app/src/views/assets/index.html
Normal file
197
actix_mvc_app/src/views/assets/index.html
Normal 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>
|
||||
286
actix_mvc_app/src/views/assets/list.html
Normal file
286
actix_mvc_app/src/views/assets/list.html
Normal file
@@ -0,0 +1,286 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Digital Assets List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Digital Assets</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">All Assets</li>
|
||||
</ol>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-filter me-1"></i>
|
||||
Filter Assets
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="assetTypeFilter" class="form-label">Asset Type</label>
|
||||
<select class="form-select" id="assetTypeFilter">
|
||||
<option value="all">All Types</option>
|
||||
<option value="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 %}
|
||||
373
actix_mvc_app/src/views/assets/my_assets.html
Normal file
373
actix_mvc_app/src/views/assets/my_assets.html
Normal file
@@ -0,0 +1,373 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}My Digital Assets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">My Digital Assets</h1>
|
||||
<ol class="breadcrumb mb-4">
|
||||
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="/assets">Digital Assets</a></li>
|
||||
<li class="breadcrumb-item active">My Assets</li>
|
||||
</ol>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-primary text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="display-4">{{ assets | length }}</h2>
|
||||
<p class="mb-0">Total Assets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-success text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set active_count = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.status == "Active" %}
|
||||
{% set active_count = active_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">{{ active_count }}</h2>
|
||||
<p class="mb-0">Active Assets</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-warning text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set for_sale_count = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.status == "For Sale" %}
|
||||
{% set for_sale_count = for_sale_count + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">{{ for_sale_count }}</h2>
|
||||
<p class="mb-0">For Sale</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card bg-info text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% set total_value = 0 %}
|
||||
{% for asset in assets %}
|
||||
{% if asset.current_valuation %}
|
||||
{% set total_value = total_value + asset.current_valuation %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<h2 class="display-4">${% if total_value %}{{ total_value }}{% else %}0.00{% endif %}</h2>
|
||||
<p class="mb-0">Total Value</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Table -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-table me-1"></i>
|
||||
My Digital Assets
|
||||
</div>
|
||||
<div>
|
||||
<a href="/assets/create" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus"></i> Create New Asset
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if assets and assets|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover" id="myAssetsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Valuation</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for asset in assets %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if asset.image_url %}
|
||||
<img src="{{ asset.image_url }}" alt="{{ asset.name }}" class="asset-thumbnail me-2">
|
||||
{% endif %}
|
||||
{{ asset.name }}
|
||||
</td>
|
||||
<td>{{ asset.asset_type }}</td>
|
||||
<td>
|
||||
{% if asset.status == "Active" %}
|
||||
<span class="badge bg-success">{{ asset.status }}</span>
|
||||
{% elif asset.status == "For Sale" %}
|
||||
<span class="badge bg-warning">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Locked" %}
|
||||
<span class="badge bg-secondary">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Transferred" %}
|
||||
<span class="badge bg-info">{{ asset.status }}</span>
|
||||
{% elif asset.status == "Archived" %}
|
||||
<span class="badge bg-danger">{{ asset.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ asset.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if asset.current_valuation %}
|
||||
{{ asset.valuation_currency }}{{ asset.current_valuation }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ asset.created_at }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/assets/{{ asset.id }}" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-warning" data-bs-toggle="modal" data-bs-target="#statusModal" data-asset-id="{{ asset.id }}">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#valuationModal" data-asset-id="{{ asset.id }}">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<p>You don't have any digital assets yet.</p>
|
||||
<a href="/assets/create" class="btn btn-primary">Create Your First Asset</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Types Distribution -->
|
||||
<div class="row">
|
||||
<div class="col-xl-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-pie me-1"></i>
|
||||
Asset Types Distribution
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="assetTypesChart" width="100%" height="40"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-chart-bar me-1"></i>
|
||||
Asset Value Distribution
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="assetValueChart" width="100%" height="40"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Change Modal -->
|
||||
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="statusModalLabel">Change Asset Status</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="statusForm" method="post" action="">
|
||||
<div class="mb-3">
|
||||
<label for="newStatus" class="form-label">New Status</label>
|
||||
<select class="form-select" id="newStatus" name="status">
|
||||
<option value="Active">Active</option>
|
||||
<option value="Locked">Locked</option>
|
||||
<option value="ForSale">For Sale</option>
|
||||
<option value="Transferred">Transferred</option>
|
||||
<option value="Archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveStatusBtn">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valuation Modal -->
|
||||
<div class="modal fade" id="valuationModal" tabindex="-1" aria-labelledby="valuationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="valuationModalLabel">Add Valuation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="valuationForm" method="post" action="">
|
||||
<div class="mb-3">
|
||||
<label for="value" class="form-label">Value</label>
|
||||
<input type="number" class="form-control" id="value" name="value" step="0.01" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="currency" class="form-label">Currency</label>
|
||||
<input type="text" class="form-control" id="currency" name="currency" value="USD" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="source" class="form-label">Source</label>
|
||||
<input type="text" class="form-control" id="source" name="source" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="notes" class="form-label">Notes</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveValuationBtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Status modal functionality
|
||||
const statusModal = document.getElementById('statusModal');
|
||||
if (statusModal) {
|
||||
statusModal.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const assetId = button.getAttribute('data-asset-id');
|
||||
const form = document.getElementById('statusForm');
|
||||
form.action = `/assets/${assetId}/status/`;
|
||||
});
|
||||
|
||||
const saveStatusBtn = document.getElementById('saveStatusBtn');
|
||||
saveStatusBtn.addEventListener('click', function() {
|
||||
const form = document.getElementById('statusForm');
|
||||
const newStatus = document.getElementById('newStatus').value;
|
||||
form.action = form.action + newStatus;
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Valuation modal functionality
|
||||
const valuationModal = document.getElementById('valuationModal');
|
||||
if (valuationModal) {
|
||||
valuationModal.addEventListener('show.bs.modal', function(event) {
|
||||
const button = event.relatedTarget;
|
||||
const assetId = button.getAttribute('data-asset-id');
|
||||
const form = document.getElementById('valuationForm');
|
||||
form.action = `/assets/${assetId}/valuation`;
|
||||
});
|
||||
|
||||
const saveValuationBtn = document.getElementById('saveValuationBtn');
|
||||
saveValuationBtn.addEventListener('click', function() {
|
||||
document.getElementById('valuationForm').submit();
|
||||
});
|
||||
}
|
||||
|
||||
// Asset Types Chart
|
||||
const assetTypesCtx = document.getElementById('assetTypesChart');
|
||||
if (assetTypesCtx) {
|
||||
// Count assets by type
|
||||
const assetTypes = {};
|
||||
{% for asset in assets %}
|
||||
if (!assetTypes['{{ asset.asset_type }}']) {
|
||||
assetTypes['{{ asset.asset_type }}'] = 0;
|
||||
}
|
||||
assetTypes['{{ asset.asset_type }}']++;
|
||||
{% endfor %}
|
||||
|
||||
const typeLabels = Object.keys(assetTypes);
|
||||
const typeCounts = Object.values(assetTypes);
|
||||
|
||||
new Chart(assetTypesCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: typeLabels,
|
||||
datasets: [{
|
||||
data: typeCounts,
|
||||
backgroundColor: [
|
||||
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e',
|
||||
'#e74a3b', '#858796', '#5a5c69', '#2c9faf'
|
||||
],
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Asset Value Chart
|
||||
const assetValueCtx = document.getElementById('assetValueChart');
|
||||
if (assetValueCtx) {
|
||||
// Prepare data for assets with valuation
|
||||
const assetNames = [];
|
||||
const assetValues = [];
|
||||
|
||||
{% for asset in assets %}
|
||||
{% if asset.current_valuation %}
|
||||
assetNames.push('{{ asset.name }}');
|
||||
assetValues.push({{ asset.current_valuation }});
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
new Chart(assetValueCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: assetNames,
|
||||
datasets: [{
|
||||
label: 'Asset Value ($)',
|
||||
data: assetValues,
|
||||
backgroundColor: '#4e73df',
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.asset-thumbnail {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - Actix MVC App{% endblock %}
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,48 +3,96 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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="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 %}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">Actix MVC App</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
<!-- Header - Full Width -->
|
||||
<header class="header bg-dark text-white">
|
||||
<div class="d-flex container-fluid justify-content-between align-items-center h-100">
|
||||
<div class="align-items-center">
|
||||
<button class="navbar-toggler d-md-none me-2" type="button" id="sidebarToggle" aria-label="Toggle navigation">
|
||||
<i class="bi bi-list text-white"></i>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'home' %}active{% endif %}" href="/">Home</a>
|
||||
<h5 class="mb-0">Zanzibar Digital Freezone {% if entity_name %}| <span class="text-info">{{ entity_name }}</span>{% endif %}</h5>
|
||||
</div>
|
||||
<div class="d-none d-md-flex">
|
||||
<ul class="navbar-nav flex-row">
|
||||
<li class="nav-item mx-3">
|
||||
<a class="nav-link text-white {% if active_page == 'about' %}active{% endif %}" target="_blank" href="https://info.ourworld.tf/zdfz">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'about' %}active{% endif %}" href="/about">About</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<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 class="nav-item mx-3">
|
||||
<a class="nav-link text-white {% if active_page == 'contact' %}active{% endif %}" href="/contact">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
</div>
|
||||
<div>
|
||||
<ul class="navbar-nav flex-row">
|
||||
{% if user and user.id %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ user.name }}
|
||||
<a class="nav-link dropdown-toggle text-white" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle"></i> {{ user.name }}
|
||||
</a>
|
||||
<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/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" %}
|
||||
<li><a class="dropdown-item" href="/admin">Admin Panel</a></li>
|
||||
{% endif %}
|
||||
@@ -53,39 +101,163 @@
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a>
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link text-white {% if active_page == 'login' %}active{% endif %}" href="/login">Login</a>
|
||||
</li>
|
||||
<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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container py-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="bg-dark text-white py-4 mt-5">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Actix MVC App</h5>
|
||||
<p>A Rust web application using Actix Web, Tera templates, and Bootstrap.</p>
|
||||
<div class="d-flex flex-column min-vh-100">
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar bg-light shadow-sm border-end d-flex" id="sidebar">
|
||||
<div class="py-2">
|
||||
<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 class="col-md-6 text-md-end">
|
||||
<p>© {{ now(year=true) }} Actix MVC App. All rights reserved.</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content flex-grow-1">
|
||||
<!-- Page Content -->
|
||||
<main class="py-3 w-100 d-block">
|
||||
<div class="container-fluid">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Footer - Full Width -->
|
||||
<footer class="footer bg-dark text-white">
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4 text-center text-md-start mb-2 mb-md-0">
|
||||
<small>Convenience, Safety and Privacy</small>
|
||||
</div>
|
||||
<div class="col-md-4 text-center mb-2 mb-md-0">
|
||||
<a class="text-white text-decoration-none mx-2" target="_blank" href="https://info.ourworld.tf/zdfz">About</a>
|
||||
<span class="text-white">|</span>
|
||||
<a class="text-white text-decoration-none mx-2" href="/contact">Contact</a>
|
||||
</div>
|
||||
<div class="col-md-4 text-center text-md-end">
|
||||
<small>© 2024 Zanzibar Digital Freezone</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="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>
|
||||
// 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 %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Calendar{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<h1>Calendar</h1>
|
||||
|
||||
<p>View Mode: {{ view_mode }}</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}New Calendar Event{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
<h1>Create New Event</h1>
|
||||
|
||||
{% if error %}
|
||||
|
||||
111
actix_mvc_app/src/views/company/index.html
Normal file
111
actix_mvc_app/src/views/company/index.html
Normal 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 %}
|
||||
193
actix_mvc_app/src/views/company/manage.html
Normal file
193
actix_mvc_app/src/views/company/manage.html
Normal 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>
|
||||
1196
actix_mvc_app/src/views/company/register.html
Normal file
1196
actix_mvc_app/src/views/company/register.html
Normal file
File diff suppressed because it is too large
Load Diff
21
actix_mvc_app/src/views/company/tabs.html
Normal file
21
actix_mvc_app/src/views/company/tabs.html
Normal 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>
|
||||
177
actix_mvc_app/src/views/company/view.html
Normal file
177
actix_mvc_app/src/views/company/view.html
Normal 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 %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Contact - Actix MVC App{% endblock %}
|
||||
{% block title %}Contact - Zanzibar Digital Freezone{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -37,15 +37,15 @@
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Email</h5>
|
||||
<p class="card-text">info@example.com</p>
|
||||
<p class="card-text">info@ourworld.tf</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">GitHub</h5>
|
||||
<p class="card-text">github.com/example/actix-mvc-app</p>
|
||||
<h5 class="card-title">Website</h5>
|
||||
<p class="card-text">https://info.ourworld.tf/zdfz</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
600
actix_mvc_app/src/views/contracts/contract_detail.html
Normal file
600
actix_mvc_app/src/views/contracts/contract_detail.html
Normal 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 %}
|
||||
140
actix_mvc_app/src/views/contracts/contracts.html
Normal file
140
actix_mvc_app/src/views/contracts/contracts.html
Normal 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 %}
|
||||
166
actix_mvc_app/src/views/contracts/create_contract.html
Normal file
166
actix_mvc_app/src/views/contracts/create_contract.html
Normal 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 %}
|
||||
187
actix_mvc_app/src/views/contracts/index.html
Normal file
187
actix_mvc_app/src/views/contracts/index.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
134
actix_mvc_app/src/views/contracts/my_contracts.html
Normal file
134
actix_mvc_app/src/views/contracts/my_contracts.html
Normal 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 %}
|
||||
138
actix_mvc_app/src/views/defi/index.html
Normal file
138
actix_mvc_app/src/views/defi/index.html
Normal 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 %}
|
||||
306
actix_mvc_app/src/views/defi/tabs/collateral.html
Normal file
306
actix_mvc_app/src/views/defi/tabs/collateral.html
Normal 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>
|
||||
281
actix_mvc_app/src/views/defi/tabs/lending_borrowing.html
Normal file
281
actix_mvc_app/src/views/defi/tabs/lending_borrowing.html
Normal 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>
|
||||
274
actix_mvc_app/src/views/defi/tabs/liquidity.html
Normal file
274
actix_mvc_app/src/views/defi/tabs/liquidity.html
Normal 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>
|
||||
8
actix_mvc_app/src/views/defi/tabs/overview.html
Normal file
8
actix_mvc_app/src/views/defi/tabs/overview.html
Normal 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>
|
||||
257
actix_mvc_app/src/views/defi/tabs/providing_receiving.html
Normal file
257
actix_mvc_app/src/views/defi/tabs/providing_receiving.html
Normal 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>
|
||||
280
actix_mvc_app/src/views/defi/tabs/staking.html
Normal file
280
actix_mvc_app/src/views/defi/tabs/staking.html
Normal 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>
|
||||
281
actix_mvc_app/src/views/defi/tabs/swap.html
Normal file
281
actix_mvc_app/src/views/defi/tabs/swap.html
Normal 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>
|
||||
53
actix_mvc_app/src/views/error.html
Normal file
53
actix_mvc_app/src/views/error.html
Normal 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 %}
|
||||
101
actix_mvc_app/src/views/flows/create_flow.html
Normal file
101
actix_mvc_app/src/views/flows/create_flow.html
Normal 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 %}
|
||||
246
actix_mvc_app/src/views/flows/flow_detail.html
Normal file
246
actix_mvc_app/src/views/flows/flow_detail.html
Normal 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 %}
|
||||
175
actix_mvc_app/src/views/flows/flows.html
Normal file
175
actix_mvc_app/src/views/flows/flows.html
Normal 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 %}
|
||||
411
actix_mvc_app/src/views/flows/index copy.html
Normal file
411
actix_mvc_app/src/views/flows/index copy.html
Normal 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 %}
|
||||
271
actix_mvc_app/src/views/flows/index.html
Normal file
271
actix_mvc_app/src/views/flows/index.html
Normal 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 %}
|
||||
116
actix_mvc_app/src/views/flows/my_flows.html
Normal file
116
actix_mvc_app/src/views/flows/my_flows.html
Normal 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 %}
|
||||
119
actix_mvc_app/src/views/governance/create_proposal.html
Normal file
119
actix_mvc_app/src/views/governance/create_proposal.html
Normal 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 %}
|
||||
172
actix_mvc_app/src/views/governance/index.html
Normal file
172
actix_mvc_app/src/views/governance/index.html
Normal 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 %}
|
||||
135
actix_mvc_app/src/views/governance/my_votes.html
Normal file
135
actix_mvc_app/src/views/governance/my_votes.html
Normal 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 %}
|
||||
189
actix_mvc_app/src/views/governance/proposal_detail.html
Normal file
189
actix_mvc_app/src/views/governance/proposal_detail.html
Normal 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 %}
|
||||
130
actix_mvc_app/src/views/governance/proposals.html
Normal file
130
actix_mvc_app/src/views/governance/proposals.html
Normal 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 %}
|
||||
@@ -1,53 +1,84 @@
|
||||
{% 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 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Welcome to Actix MVC App</h1>
|
||||
<p class="card-text">This is a Rust web application built with:</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>
|
||||
<li class="list-group-item">Tera Templates - A template engine inspired by Jinja2 and Django templates</li>
|
||||
<li class="list-group-item">Bootstrap 5.3.5 - A popular CSS framework for responsive web design</li>
|
||||
</ul>
|
||||
<p>This application follows the MVC (Model-View-Controller) architectural pattern:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Models
|
||||
<h1 class="card-title text-center mb-4">Zanzibar Digital Freezone</h1>
|
||||
<p class="card-text text-center lead mb-5">Convenience, Safety and Privacy</p>
|
||||
|
||||
<style>
|
||||
.compact-card {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- Left Column (3 items) -->
|
||||
<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 class="card-body">
|
||||
<p>Data structures and business logic</p>
|
||||
<div class="card-body p-2 compact-card">
|
||||
<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 class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-success text-white">
|
||||
Views
|
||||
|
||||
<!-- Right Column (2 items) -->
|
||||
<div class="col-md-6">
|
||||
<!-- 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 class="card-body">
|
||||
<p>Tera templates for rendering HTML</p>
|
||||
<div class="card-body p-2 compact-card">
|
||||
<p class="card-text small">Easily expand your business globally with streamlined operations and tools to reach customers worldwide.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 5: Clear Regulations -->
|
||||
<div class="card shadow mb-3 border-danger">
|
||||
<div class="card-header py-2 bg-danger bg-opacity-10 border-danger">
|
||||
<h6 class="mb-0 text-danger"><i class="bi bi-shield-check me-2"></i>Clear Regulations</h6>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
Controllers
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Request handlers and application logic</p>
|
||||
<div class="card-body p-2 compact-card">
|
||||
<p class="card-text small">Clear regulations and efficient dispute resolution mechanisms providing a stable business environment.</p>
|
||||
</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>
|
||||
|
||||
236
actix_mvc_app/src/views/marketplace/create_listing.html
Normal file
236
actix_mvc_app/src/views/marketplace/create_listing.html
Normal 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 %}
|
||||
293
actix_mvc_app/src/views/marketplace/index.html
Normal file
293
actix_mvc_app/src/views/marketplace/index.html
Normal 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 %}
|
||||
356
actix_mvc_app/src/views/marketplace/listing_detail.html
Normal file
356
actix_mvc_app/src/views/marketplace/listing_detail.html
Normal 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 %}
|
||||
294
actix_mvc_app/src/views/marketplace/listings.html
Normal file
294
actix_mvc_app/src/views/marketplace/listings.html
Normal 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 %}
|
||||
238
actix_mvc_app/src/views/marketplace/my_listings.html
Normal file
238
actix_mvc_app/src/views/marketplace/my_listings.html
Normal 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 %}
|
||||
10
actix_mvc_app/src/views/test.html
Normal file
10
actix_mvc_app/src/views/test.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test Page</h1>
|
||||
<p>This is a simple test page to verify template rendering.</p>
|
||||
</body>
|
||||
</html>
|
||||
10
actix_mvc_app/src/views/test_base.html
Normal file
10
actix_mvc_app/src/views/test_base.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Test Base Template{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid px-4">
|
||||
<h1 class="mt-4">Test Base Template</h1>
|
||||
<p>This is a simplified template for testing that extends base.html.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Support Tickets - Actix MVC App{% endblock %}
|
||||
{% block title %}Support Tickets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}My Tickets - Actix MVC App{% endblock %}
|
||||
{% block title %}My Tickets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Support Ticket - Actix MVC App{% endblock %}
|
||||
{% block title %}New Support Ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{% 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 %}
|
||||
<div class="container" up-main>
|
||||
<div class="container-fluid" up-main>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Ticket #{{ ticket.id | truncate(length=8) }}</h1>
|
||||
<div>
|
||||
|
||||
BIN
actix_mvc_app/static/img/tokens/default.png
Normal file
BIN
actix_mvc_app/static/img/tokens/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 314 B |
BIN
actix_mvc_app/static/img/tokens/tft.png
Normal file
BIN
actix_mvc_app/static/img/tokens/tft.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 314 B |
BIN
actix_mvc_app/static/img/tokens/usdt.png
Normal file
BIN
actix_mvc_app/static/img/tokens/usdt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 314 B |
BIN
actix_mvc_app/static/img/tokens/zaz.png
Normal file
BIN
actix_mvc_app/static/img/tokens/zaz.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 314 B |
173
actix_mvc_app/static/js/company.js
Normal file
173
actix_mvc_app/static/js/company.js
Normal 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');
|
||||
});
|
||||
562
actix_mvc_app/static/js/defi.js
Normal file
562
actix_mvc_app/static/js/defi.js
Normal 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
2809
flowbroker/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
flowbroker/Cargo.toml
Normal file
27
flowbroker/Cargo.toml
Normal 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'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user