Skip to main content

web/pages/account/
edit.rs

1use askama::Template;
2use axum::{
3    Extension, Json,
4    extract::{Path, State},
5    http::StatusCode,
6    response::IntoResponse,
7};
8use finance::account::Account;
9use serde::Deserialize;
10use server::command::{CmdResult, FinanceEntity, account::GetAccount};
11use sqlx::types::Uuid;
12use std::sync::Arc;
13
14use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
15
16struct ScriptView {
17    id: Uuid,
18    name: Option<String>,
19}
20
21#[derive(Template)]
22#[template(path = "pages/account/edit.html")]
23struct AccountEditPage {
24    account_id: Uuid,
25    account_name: String,
26    tags: Vec<finance::tag::Tag>,
27    scripting_enabled: bool,
28    scripts: Vec<ScriptView>,
29}
30
31pub async fn account_edit_page(
32    Path(id): Path<Uuid>,
33    State(_data): State<Arc<AppState>>,
34    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
35) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
36    let user = &jwt_auth.user;
37
38    let account_result = GetAccount::new()
39        .user_id(user.id)
40        .account_id(id)
41        .run()
42        .await
43        .map_err(|e| {
44            let error_response = serde_json::json!({
45                "status": "fail",
46                "message": format!("Failed to get account: {e:?}"),
47            });
48            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
49        })?;
50
51    let (account, name) = if let Some(CmdResult::TaggedEntities { entities, .. }) = account_result
52        && let Some((FinanceEntity::Account(account), tags)) = entities.into_iter().next()
53    {
54        let name = if let Some(FinanceEntity::Tag(name_tag)) = tags.get("name") {
55            name_tag.tag_value.clone()
56        } else {
57            String::new()
58        };
59        (account, name)
60    } else {
61        let error_response = serde_json::json!({
62            "status": "fail",
63            "message": "Account not found",
64        });
65        return Err((StatusCode::NOT_FOUND, Json(error_response)));
66    };
67
68    let server_user = server::user::User { id: user.id };
69    let tags: Vec<finance::tag::Tag> = server_user
70        .get_account_tags(&account)
71        .await
72        .unwrap_or_default()
73        .into_iter()
74        .filter(|t| t.tag_name != "name")
75        .collect();
76
77    #[cfg(feature = "scripting")]
78    let scripts: Vec<ScriptView> = server_user
79        .list_scripts()
80        .await
81        .unwrap_or_default()
82        .into_iter()
83        .map(|s| ScriptView {
84            id: s.id,
85            name: s.name,
86        })
87        .collect();
88
89    #[cfg(not(feature = "scripting"))]
90    let scripts: Vec<ScriptView> = Vec::new();
91
92    let template = AccountEditPage {
93        account_id: account.id,
94        account_name: name,
95        tags,
96        scripting_enabled: cfg!(feature = "scripting"),
97        scripts,
98    };
99
100    Ok(HtmlTemplate(template))
101}
102
103#[derive(Deserialize)]
104pub struct RenameForm {
105    account_id: Uuid,
106    name: String,
107}
108
109#[derive(Deserialize)]
110struct AccountTagData {
111    name: String,
112    value: String,
113    description: Option<String>,
114}
115
116#[derive(Deserialize)]
117pub struct AccountTagsForm {
118    tags: Vec<AccountTagData>,
119}
120
121pub async fn rename_account(
122    State(_data): State<Arc<AppState>>,
123    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
124    Json(form): Json<RenameForm>,
125) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
126    let user = &jwt_auth.user;
127    let server_user = server::user::User { id: user.id };
128
129    let account = Account {
130        id: form.account_id,
131        parent: None,
132    };
133
134    let name_tag = finance::tag::Tag {
135        id: Uuid::new_v4(),
136        tag_name: "name".to_string(),
137        tag_value: form.name,
138        description: None,
139    };
140
141    server_user
142        .set_account_tag(&account, &name_tag)
143        .await
144        .map_err(|e| {
145            let error_response = serde_json::json!({
146                "status": "fail",
147                "message": t!("Failed to rename account"),
148            });
149            log::error!("Failed to rename account: {e:?}");
150            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
151        })?;
152
153    Ok(t!("Account renamed").to_string())
154}
155
156pub async fn account_tags_submit(
157    Path(id): Path<Uuid>,
158    State(_data): State<Arc<AppState>>,
159    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
160    Json(form): Json<AccountTagsForm>,
161) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
162    let user = &jwt_auth.user;
163    let server_user = server::user::User { id: user.id };
164
165    let account = Account { id, parent: None };
166
167    let existing_tags = server_user
168        .get_account_tags(&account)
169        .await
170        .unwrap_or_default();
171
172    for tag in &existing_tags {
173        if tag.tag_name == "name" {
174            continue;
175        }
176        let _ = server_user.detach_account_tag(id, tag.id).await;
177        let _ = server_user.cleanup_orphan_tag(tag.id).await;
178    }
179
180    for tag_data in form.tags {
181        if tag_data.name == "name" {
182            continue;
183        }
184        server_user
185            .create_account_tag(id, tag_data.name, tag_data.value, tag_data.description)
186            .await
187            .map_err(|e| {
188                let error_response = serde_json::json!({
189                    "status": "fail",
190                    "message": format!("Failed to create account tag: {:?}", e),
191                });
192                log::error!("Failed to create account tag: {e:?}");
193                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
194            })?;
195    }
196
197    Ok(t!("Account tags saved").to_string())
198}
199
200#[cfg(feature = "scripting")]
201pub async fn run_account_script(
202    Path((account_id, script_id)): Path<(Uuid, Uuid)>,
203    State(_data): State<Arc<AppState>>,
204    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
205) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
206    use scripting::ScriptExecutor;
207    use server::script::TransactionState;
208
209    let user = &jwt_auth.user;
210    let server_user = server::user::User { id: user.id };
211
212    let script = server_user.get_script(script_id).await.map_err(|e| {
213        let error_response = serde_json::json!({
214            "status": "fail",
215            "message": format!("Failed to get script: {e:?}"),
216        });
217        (StatusCode::NOT_FOUND, Json(error_response))
218    })?;
219
220    let transaction_ids = server_user
221        .list_transaction_ids_by_account(account_id)
222        .await
223        .map_err(|e| {
224            let error_response = serde_json::json!({
225                "status": "fail",
226                "message": format!("Failed to fetch transactions: {e:?}"),
227            });
228            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
229        })?;
230
231    let executor = ScriptExecutor::new();
232    let mut processed = 0u64;
233
234    for tx_id in &transaction_ids {
235        let tx_result = server::command::transaction::GetTransaction::new()
236            .user_id(user.id)
237            .transaction_id(*tx_id)
238            .run()
239            .await
240            .map_err(|e| {
241                let error_response = serde_json::json!({
242                    "status": "fail",
243                    "message": format!("Failed to get transaction {tx_id}: {e:?}"),
244                });
245                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
246            })?;
247
248        let (transaction, note) = if let Some(CmdResult::TaggedEntities { mut entities, .. }) =
249            tx_result
250            && let Some((FinanceEntity::Transaction(tx), tags)) = entities.pop()
251        {
252            let note = tags.get("note").and_then(|entity| {
253                if let FinanceEntity::Tag(tag) = entity {
254                    Some(tag.tag_value.clone())
255                } else {
256                    None
257                }
258            });
259            (tx, note)
260        } else {
261            continue;
262        };
263
264        let splits_result = server::command::split::ListSplits::new()
265            .user_id(user.id)
266            .transaction(*tx_id)
267            .run()
268            .await
269            .map_err(|e| {
270                let error_response = serde_json::json!({
271                    "status": "fail",
272                    "message": format!("Failed to get splits for {tx_id}: {e:?}"),
273                });
274                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
275            })?;
276
277        let mut split_entities = Vec::new();
278        if let Some(CmdResult::TaggedEntities {
279            entities: split_data,
280            ..
281        }) = splits_result
282        {
283            for (entity, _tags) in split_data {
284                split_entities.push(entity);
285            }
286        }
287
288        let state = TransactionState::new(transaction)
289            .with(split_entities)
290            .with_note(note);
291
292        let state = state
293            .run_scripts(&executor, &[(script.id, script.bytecode.clone())])
294            .map_err(|e| {
295                let error_response = serde_json::json!({
296                    "status": "fail",
297                    "message": format!("Script execution failed on {tx_id}: {e:?}"),
298                });
299                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
300            })?;
301
302        for tag in &state.transaction_tags {
303            let _ = server_user
304                .create_transaction_tag(*tx_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
305                .await;
306        }
307
308        for (split_id, tag) in &state.split_tags {
309            let _ = server_user
310                .create_split_tag(*split_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
311                .await;
312        }
313
314        processed += 1;
315    }
316
317    Ok(format!("{}: {processed}", t!("Processed transactions")))
318}