Added nice chart for account history.

This commit is contained in:
didier amyot committed 2023-04-16 11:34:53 -04:00
1 parent 80153dd6ad
commit e76a521356
14 files changed
+126 -188

No files matched your search

+9 -3
View File
@@ -10,7 +10,7 @@ nix build
### Building dev environment ### Building dev environment
#### For ide #### For IDE
```shell ```shell
nix build .#development-environment nix build .#development-environment
@@ -18,14 +18,20 @@ nix build .#development-environment
Then, in IntelliJ use `result/bin` Then, in IntelliJ use `result/bin`
The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/` The rust source must be `/nix/store/pmih7jv9r5xfaif92bza8w05876qvnvc-rust-src/` or similar.
#### For shell #### For development shell
```shell ```shell
nix develop nix develop
``` ```
#### Init Postgres
```shell
```
## Technologies ## Technologies
### Front-end ### Front-end
@@ -80,12 +80,13 @@ CREATE TABLE IF NOT EXISTS accounts
CREATE TABLE IF NOT EXISTS accounts_history CREATE TABLE IF NOT EXISTS accounts_history
( (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL, account_id INTEGER NOT NULL,
account_version INTEGER NOT NULL, account_version INTEGER NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
timestamp DATETIME NOT NULL, timestamp DATETIME NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts (id), FOREIGN KEY (account_id) REFERENCES accounts (id),
PRIMARY KEY (account_id, account_version) UNIQUE (account_id, account_version)
); );
CREATE TABLE IF NOT EXISTS account_user_permissions CREATE TABLE IF NOT EXISTS account_user_permissions
+22 -48
View File
@@ -15,60 +15,34 @@
<tr><th>Account Type</th><td>{{ account.account_type | replace("_", " ") | lower}}</td></tr> <tr><th>Account Type</th><td>{{ account.account_type | replace("_", " ") | lower}}</td></tr>
<tr><th>Last Update</th><td>{{ account.last_update | utc_date }}</td></tr> <tr><th>Last Update</th><td>{{ account.last_update | utc_date }}</td></tr>
<tr><th>Security</th><td>{{ account.security.symbol }}</td></tr> <tr><th>Security</th><td>{{ account.security.symbol }}</td></tr>
<tr><th>Data</th><td>
<a href="/data/accounts/{{ account.id }}/amount-history.json">data</a>
</td></tr>
</table> </table>
<canvas id="account-history-chart"></canvas> <div id="account-history-chart-2" style="width: 100%"></div>
<script> <script>
const ctx = document.getElementById('account-history-chart'); var vlSpec = {
const labels = {{account_history.timestamps | tojson }}; "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
const datapoints = {{ account_history.amounts | tojson }}; title: "History of {{ account.id }}",
const data = { "width": "container",
labels: labels.map(x => new Date(1000 * x)), "height": 250,
datasets: [ "data": {url: "/data/accounts/{{ account.id }}/amount-history.json",
{ format:{
label: {{ account.id | micro_id | tojson }}, type: "json",
data: datapoints, property: "snapshots"
fill: false, }
tension: 0.4 },
} "mark": "line",
] "encoding": {
"x": {"field": "timestamp", "type": "temporal", title: 'Time'},
"y": {"field": "amount", "type": "quantitative", title: 'Quantity'}
}
}; };
const config = { // Embed the visualization in the container with id `vis`
type: 'line', vegaEmbed('#account-history-chart-2', vlSpec);
data: data,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Account Amount History'
},
},
interaction: {
intersect: false,
},
scales: {
x: {
display: true,
type: 'time',
title: {
display: true
}
},
y: {
display: true,
title: {
display: true,
text: 'Value'
},
suggestedMin: 0,
}
}
},
};
new Chart(ctx, config);
</script> </script>
{% endblock %} {% endblock %}
+4 -3
View File
@@ -18,9 +18,10 @@
<script src="/static/vendor/htmx.org@1.8.5"></script> <script src="/static/vendor/htmx.org@1.8.5"></script>
<script src="/static/embedded-json-enc.js"></script> <script src="/static/embedded-json-enc.js"></script>
{% if script_chart_js %} {% if script_chart_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/vega@5.22.1"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.3.0/build/global/luxon.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vega-lite@5.6.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon"></script> <script src="https://cdn.jsdelivr.net/npm/vega-embed@6.21.2"></script>
{% endif %} {% endif %}
</head> </head>
@@ -29,7 +29,6 @@
</select> </select>
</label> </label>
</div> </div>
<input type="hidden" name="event.CreateNewSecurity.user_id" value="{{ user.id }}"> <input type="hidden" name="event.CreateNewSecurity.user_id" value="{{ user.id }}">
{%include "partials/actions/submit_button.html"%} {%include "partials/actions/submit_button.html"%}
-99
View File
@@ -1,99 +0,0 @@
use rusqlite::{Connection, OptionalExtension, params};
use crate::db::accounts;
use crate::db::accounts::Account;
use crate::db::external_id::AccountId;
use crate::util::{CustomResult, Error};
pub struct AccountHistory {
account: Account,
timestamps: Vec<i64>,
amounts: Vec<i64>,
}
impl AccountHistory {
pub fn account(&self) -> &Account {
&self.account
}
pub fn timestamps(&self) -> &Vec<i64> {
&self.timestamps
}
pub fn amount(&self) -> &Vec<i64> {
&self.amounts
}
}
pub fn get_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult<AccountHistory> {
let q = "
SELECT amount, UNIXEPOCH(timestamp) AS timestamp
FROM accounts_history
WHERE account_id = ?1
ORDER BY account_version ASC
";
let mut stmt = tx.prepare(q).map_err(Error::SqlError)?;
let rows = stmt
.query_map(params![account_id], |row| Ok((row.get("amount")?, row.get("timestamp")?)))
.optional()
.map_err(Error::SqlError)?;
let ret: Vec<(i64, i64)> = rows.map(|x|
x.map(Result::unwrap)
.collect()).unwrap_or(vec![]);
let account = accounts::get_account_by_id(tx, account_id)?;
let mut amounts: Vec<i64> = ret.iter().map(|r| r.0).collect();
let mut timestamps: Vec<i64> = ret.iter().map(|r| r.1).collect();
amounts.push(account.amount());
timestamps.push(account.timestamp());
Ok(AccountHistory{account, timestamps, amounts })
}
#[cfg(test)]
mod test_account_history {
use crate::db::test_utils::setup_basic_environment;
use crate::migration;
use crate::util::CustomResult;
use super::*;
fn setup() -> CustomResult<(Connection, Account)> {
let mut conn = Connection::open_in_memory().unwrap();
migration::apply_migrations(&mut conn).unwrap();
let ret = {
let mut tx = conn.transaction().unwrap();
let (_, _, accounts) = setup_basic_environment(&mut tx)?;
tx.commit().unwrap();
accounts.get(0).unwrap().clone()
};
Ok((conn, ret))
}
#[test]
fn test_get_account_history_trivial() -> CustomResult<()>{
let (mut conn, acc) = setup()?;
let acc_hist = get_account_history(&conn, &acc.id())?;
assert_eq!(acc_hist.amounts, vec![0, 0]);
assert_eq!(acc_hist.timestamps.len(), 2);
assert_eq!(acc_hist.account.label(), "acc1");
Ok(())
}
#[test]
fn test_get_account_history_with_transactions() -> CustomResult<()>{
let (mut conn, acc) = setup()?;
{
let tx = conn.transaction().unwrap();
accounts::add_to_account(&tx, &acc.id(), 1)?;
tx.commit().unwrap();
}
let tx = conn.transaction().unwrap();
let acc_hist = get_account_history(&tx, &acc.id())?;
assert_eq!(acc_hist.amounts, vec![0, 1, 1]);
assert_eq!(acc_hist.timestamps.len(), 3);
assert_eq!(acc_hist.account.label(), "acc1");
Ok(())
}
}
+22 -5
View File
@@ -120,7 +120,7 @@ impl Clone for Account {
} }
} }
#[derive(Debug)] #[derive(Debug)]
struct AccountSnapshot { pub struct AccountSnapshot {
amount: i64, amount: i64,
version: i64, version: i64,
timestamp: i64, timestamp: i64,
@@ -134,9 +134,18 @@ impl AccountSnapshot {
timestamp: row.get("timestamp")?, timestamp: row.get("timestamp")?,
}) })
} }
pub fn amount(&self) -> i64 {
self.amount
}
pub fn version(&self) -> i64 {
self.version
}
pub fn timestamp(&self) -> i64 {
self.timestamp
}
} }
struct AccountHistory { pub struct AccountHistory {
id: i64, id: i64,
security: Security, security: Security,
account_type: AccountType, account_type: AccountType,
@@ -165,6 +174,15 @@ impl AccountHistory {
snapshots, snapshots,
}) })
} }
pub fn security(&self) -> &Security {
&self.security
}
pub fn account_type(&self) -> &AccountType {
&self.account_type
}
pub fn snapshots(&self) -> &Vec<AccountSnapshot> {
&self.snapshots
}
} }
pub fn list_accounts<'a>(conn: &Connection) -> CustomResult<Vec<Account>> { pub fn list_accounts<'a>(conn: &Connection) -> CustomResult<Vec<Account>> {
@@ -289,7 +307,7 @@ fn create_account(
return get_account_by_id(tx, &id); return get_account_by_id(tx, &id);
} }
fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult<AccountHistory> { pub fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult<AccountHistory> {
let q = " let q = "
SELECT account_version, SELECT account_version,
amount, amount,
@@ -361,7 +379,7 @@ impl AccountChangeReceipt {
fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult<()> { fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult<()> {
let q = " let q = "
INSERT INTO accounts_history INSERT INTO accounts_history(account_id, account_version, amount, timestamp)
SELECT id AS account_id, SELECT id AS account_id,
version AS account_version, version AS account_version,
amount, amount,
@@ -630,7 +648,6 @@ mod test_accounts {
let (mut conn, users, secs) = setup(); let (mut conn, users, secs) = setup();
let sec = secs.get(0).unwrap().id(); let sec = secs.get(0).unwrap().id();
let user = users.get(0).unwrap().id(); let user = users.get(0).unwrap().id();
// let sec_emitter_acc = create_security_emitter_account(&conn, &sec, &user, & "sec emitter acc".to_string())?;
let checking_account = let checking_account =
create_checking_account(&conn, &sec, &user, &"checking acc".to_string())?; create_checking_account(&conn, &sec, &user, &"checking acc".to_string())?;
match add_to_account(&conn, &checking_account.id(), -33) { match add_to_account(&conn, &checking_account.id(), -33) {
+1 -11
View File
@@ -6,7 +6,6 @@ mod _ledger_internal;
mod _securities; mod _securities;
mod _test_utils; mod _test_utils;
mod _users; mod _users;
mod _account_history;
pub mod ledger_internal { pub mod ledger_internal {
pub use super::_ledger_internal::*; pub use super::_ledger_internal::*;
@@ -27,16 +26,7 @@ pub mod users {
} }
pub mod accounts { pub mod accounts {
pub use super::_accounts::{ pub use super::_accounts::*;
add_to_account, create_checking_account, create_security_emitter_account,
get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt,
list_user_accounts, AccountType, get_checking_accounts_for_sec_id, get_number_of_accounts,
get_security_market_cap
};
}
pub mod account_history{
pub use super::_account_history::get_account_history;
} }
pub mod cheques { pub mod cheques {
+31 -13
View File
@@ -1,10 +1,10 @@
use rusqlite::Connection; use rusqlite::Connection;
use crate::db::accounts::{Account, AccountType, get_checking_accounts_for_sec_id, list_user_accounts}; use crate::db::accounts::{Account, AccountHistory, AccountType, get_checking_accounts_for_sec_id, list_user_accounts};
use crate::db::external_id::{AccountId, Id, SecurityId, UserId}; use crate::db::external_id::{AccountId, Id, SecurityId, UserId};
use crate::util::{CustomResult, Error}; use crate::util::{CustomResult, Error};
use serde::Deserialize; use serde::Deserialize;
use serde::Serialize; use serde::Serialize;
use crate::db::{account_history, accounts}; use crate::db::{accounts};
use crate::db::securities::Security; use crate::db::securities::Security;
use crate::services::_securities::SecurityDTO; use crate::services::_securities::SecurityDTO;
@@ -28,9 +28,32 @@ pub struct AccountDTO {
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct AccountHistoryDTO { pub struct AccountSnapshotDTO{
amounts: Vec<i64>, amount: i64,
timestamps: Vec<i64>, timestamp: i64,
}
#[derive(Serialize, Debug)]
pub struct AccountHistoryDTO2{
id: String,
security: SecurityDTO,
account_type: String,
snapshots: Vec<AccountSnapshotDTO>
}
impl AccountHistoryDTO2 {
fn from_entity(entity: &AccountHistory) -> AccountHistoryDTO2{
let snapshots = entity.snapshots()
.iter()
.map(|x| AccountSnapshotDTO{amount: x.amount(), timestamp: x.timestamp() * 1000})
.collect();
AccountHistoryDTO2{
id: entity.id().to_string(),
security: SecurityDTO::from_entity(entity.security()),
account_type: entity.account_type().to_string(),
snapshots
}
}
} }
impl AccountDTO { impl AccountDTO {
@@ -76,14 +99,9 @@ impl AccountService<'_> {
Ok(AccountDTO::from_entity(ret)) Ok(AccountDTO::from_entity(ret))
} }
pub fn get_account_history_by_id(&self, account_id: &AccountId, user_id: &UserId) -> CustomResult<AccountHistoryDTO> { pub fn get_account_history_by_id_2(&self, account_id: &AccountId, user_id: &UserId) -> CustomResult<AccountHistoryDTO2> {
let ac = account_history::get_account_history(self.conn, account_id)?; let ac = accounts::get_account_history(self.conn, account_id)?;
// ret.account() Ok(AccountHistoryDTO2::from_entity(&ac))
let ret = AccountHistoryDTO{
amounts: ac.amount().clone(),
timestamps: ac.timestamps().clone(),
};
Ok(ret)
} }
pub fn get_number_of_accounts(&self, sec_id: &SecurityId) -> CustomResult<AccountNumbers>{ pub fn get_number_of_accounts(&self, sec_id: &SecurityId) -> CustomResult<AccountNumbers>{
+1 -1
View File
@@ -1,7 +1,7 @@
pub use crate::services::_authentication::SessionService; pub use crate::services::_authentication::SessionService;
pub use crate::services::_securities::SecurityService; pub use crate::services::_securities::SecurityService;
pub use crate::services::_users::UserService; pub use crate::services::_users::UserService;
pub use crate::services::_accounts::AccountService; pub use crate::services::_accounts::{AccountService};
pub use crate::services::_transactions::cash_wire_transfer_request; pub use crate::services::_transactions::cash_wire_transfer_request;
mod _authentication; mod _authentication;
+30
View File
@@ -0,0 +1,30 @@
use crate::db::external_id::{AccountId, Id};
use crate::webui::services::ServicesFactory;
use crate::webui::templates::MiniJinjaRenderer;
use actix_web::{error, web, HttpRequest, Responder, Result};
use serde::Serialize;
struct ResponseData{
}
#[actix_web::get("/data/accounts/{account_id}/amount-history.json")]
pub async fn get_account_amount_history(
account_id: web::Path<String>,
req: HttpRequest,
service_factory: web::Data<ServicesFactory>,
) -> Result<impl Responder> {
let account_id = AccountId::from_string(&account_id.to_string()).unwrap();
let conn = service_factory.get_connection()?;
let auth_service = service_factory.get_authentication_service(&conn)?;
let conn = service_factory.get_connection()?;
let account_service = service_factory.get_account_service(&conn)?;
let user_id = auth_service.get_login(&req).unwrap();
let ac = account_service
.get_account_history_by_id_2(&account_id, &user_id)
.map_err(error::ErrorInternalServerError)?;
Ok(web::Json(ac))
}
+2
View File
@@ -28,6 +28,7 @@ mod templates;
mod views; mod views;
mod worker; mod worker;
mod minininja_utils; mod minininja_utils;
mod data_views;
struct ProductionEventProcessor; struct ProductionEventProcessor;
@@ -78,6 +79,7 @@ fn create_views_scope() -> Scope {
.service(views::overview_submit_button) .service(views::overview_submit_button)
.service(views::actions_partials) .service(views::actions_partials)
.service(views::get_accounts_by_security_selectors) .service(views::get_accounts_by_security_selectors)
.service(data_views::get_account_amount_history)
} }
#[tokio::main] #[tokio::main]
-3
View File
@@ -46,13 +46,10 @@ pub async fn get_account(tmpl_env: MiniJinjaRenderer,
let ctx = context!{ let ctx = context!{
account => account_service.get_account_by_id(&account_id, &user_id).map_err(error::ErrorInternalServerError)?, account => account_service.get_account_by_id(&account_id, &user_id).map_err(error::ErrorInternalServerError)?,
account_history => account_service.get_account_history_by_id(&account_id, &user_id).map_err(error::ErrorInternalServerError)?
}; };
tmpl_env.render("account.html", ctx) tmpl_env.render("account.html", ctx)
} }
#[actix_web::get("/securities")] #[actix_web::get("/securities")]
pub async fn securities(tmpl_env: MiniJinjaRenderer, pub async fn securities(tmpl_env: MiniJinjaRenderer,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> { service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env bash
cargo run users new --name didier.amyot@gmail.com --public-key fooof