1
use std::sync::Arc;
2

            
3
use askama::Template;
4
use axum::extract::Query;
5
use axum::{Extension, extract::State, http::StatusCode, response::IntoResponse};
6
use num_rational::Rational64;
7
use serde::Deserialize;
8
use server::command::{
9
    CmdResult, FinanceEntity, commodity::GetCommodity, transaction::ListTransactions,
10
};
11
use sqlx::types::Uuid;
12
use sqlx::types::chrono::{DateTime, Utc};
13

            
14
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
15

            
16
#[derive(Template)]
17
#[template(path = "pages/transaction/list.html")]
18
struct TransactionListPage {
19
    account: Option<Uuid>,
20
}
21

            
22
3
pub async fn transaction_list_page(Query(params): Query<TransactionParam>) -> impl IntoResponse {
23
2
    let template = TransactionListPage {
24
2
        account: params.account,
25
2
    };
26
2
    HtmlTemplate(template)
27
2
}
28

            
29
struct SplitView {
30
    account_id: Uuid,
31
    account_name: String,
32
    amount: String,
33
    currency: String,
34
    tags: Vec<finance::tag::Tag>,
35
}
36

            
37
struct TransactionView {
38
    id: Uuid,
39
    date: DateTime<Utc>,
40
    description: String,
41
    amount: String,
42
    currency: String,
43
    splits: Vec<SplitView>,
44
}
45

            
46
#[derive(Template)]
47
#[template(path = "components/transaction/table.html")]
48
struct TransactionTableTemplate {
49
    transactions: Vec<TransactionView>,
50
}
51

            
52
#[derive(Deserialize)]
53
pub struct TransactionParam {
54
    account: Option<Uuid>,
55
}
56

            
57
10
pub async fn transaction_table(
58
10
    Query(transactionparam): Query<TransactionParam>,
59
10
    State(_data): State<Arc<AppState>>,
60
10
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
61
15
) -> Result<impl IntoResponse, StatusCode> {
62
10
    let mut cmd = ListTransactions::new().user_id(jwt_auth.user.id);
63

            
64
10
    if let Some(id) = transactionparam.account {
65
4
        cmd = cmd.account(id);
66
6
    }
67

            
68
10
    let result = cmd
69
10
        .run()
70
10
        .await
71
10
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
72

            
73
    let mut transactions = Vec::new();
74

            
75
    if let Some(CmdResult::TaggedEntities(entities)) = result {
76
        for (entity, tags) in entities {
77
            if let FinanceEntity::Transaction(tx) = entity {
78
                // Get description from note tag or use default
79
                let description = if let Some(FinanceEntity::Tag(note)) = tags.get("note") {
80
                    note.tag_value.clone()
81
                } else {
82
                    format!("Transaction {}", tx.id)
83
                };
84

            
85
                // Get splits for this transaction
86
                let split_result = server::command::split::ListSplits::new()
87
                    .user_id(jwt_auth.user.id)
88
                    .transaction(tx.id)
89
                    .run()
90
                    .await
91
                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
92

            
93
                // Calculate total amount and get currency
94
                let mut total = Rational64::new(0, 1);
95
                let mut currency = String::new();
96
                let mut splits = Vec::new();
97

            
98
                if let Some(CmdResult::TaggedEntities(split_entities)) = split_result {
99
                    for (split_entity, split_tags) in split_entities {
100
                        if let FinanceEntity::Split(split) = split_entity {
101
                            let split_amount = Rational64::new(split.value_num, split.value_denom);
102
                            let mut account_name = format!("Account {}", split.account_id);
103
                            let mut split_currency = String::new();
104

            
105
                            // For filtered view, show amount from the filtered account's split
106
                            // For unfiltered view, sum positive values to get transaction total
107
                            let is_filtered_account =
108
                                transactionparam.account == Some(split.account_id);
109
                            let should_count = if transactionparam.account.is_some() {
110
                                is_filtered_account
111
                            } else {
112
                                split.value_num > 0
113
                            };
114

            
115
                            if should_count {
116
                                total += split_amount;
117
                            }
118

            
119
                            // Get account name and currency for this split
120
                            if let Ok(Some(CmdResult::TaggedEntities(account_entities))) =
121
                                server::command::account::GetAccount::new()
122
                                    .user_id(jwt_auth.user.id)
123
                                    .account_id(split.account_id)
124
                                    .run()
125
                                    .await
126
                                && let Some((FinanceEntity::Account(_account), account_tags)) =
127
                                    account_entities.first()
128
                            {
129
                                if let Some(FinanceEntity::Tag(name)) = account_tags.get("name") {
130
                                    account_name = name.tag_value.clone();
131
                                }
132

            
133
                                // Get commodity for this split
134
                                if let Ok(Some(CmdResult::TaggedEntities(commodity_entities))) =
135
                                    GetCommodity::new()
136
                                        .user_id(jwt_auth.user.id)
137
                                        .commodity_id(split.commodity_id)
138
                                        .run()
139
                                        .await
140
                                    && let Some((_, commodity_tags)) = commodity_entities.first()
141
                                    && let Some(FinanceEntity::Tag(symbol)) =
142
                                        commodity_tags.get("symbol")
143
                                {
144
                                    split_currency = symbol.tag_value.clone();
145

            
146
                                    // Set transaction currency based on what we're counting
147
                                    if currency.is_empty() && should_count {
148
                                        currency = split_currency.clone();
149
                                    }
150
                                }
151
                            }
152

            
153
                            // Format the split amount - preserve the sign for template logic
154
                            let split_amount_str = if *split_amount.denom() == 1 {
155
                                split_amount.numer().to_string()
156
                            } else {
157
                                let value =
158
                                    *split_amount.numer() as f64 / *split_amount.denom() as f64;
159
                                format!("{value:.2}")
160
                            };
161

            
162
                            // Extract tags from the split_tags HashMap by taking ownership
163
                            let mut tags: Vec<finance::tag::Tag> = split_tags
164
                                .into_values()
165
                                .filter_map(|entity| {
166
                                    if let FinanceEntity::Tag(tag) = entity {
167
                                        Some(tag)
168
                                    } else {
169
                                        None
170
                                    }
171
                                })
172
                                .collect();
173

            
174
                            // Sort tags by name, then by value for stable display
175
                            tags.sort_by(|a, b| {
176
                                a.tag_name
177
                                    .cmp(&b.tag_name)
178
                                    .then_with(|| a.tag_value.cmp(&b.tag_value))
179
                            });
180

            
181
                            // Add the split with its account name
182
                            splits.push(SplitView {
183
                                account_id: split.account_id,
184
                                account_name,
185
                                amount: split_amount_str,
186
                                currency: split_currency,
187
                                tags,
188
                            });
189
                        }
190
                    }
191
                }
192

            
193
                // Format the amount with currency
194
                let amount_str = if *total.denom() == 1 {
195
                    total.numer().to_string()
196
                } else {
197
                    format!("{:.2}", *total.numer() as f64 / *total.denom() as f64)
198
                };
199

            
200
                transactions.push(TransactionView {
201
                    id: tx.id,
202
                    date: tx.post_date,
203
                    description,
204
                    amount: amount_str,
205
                    currency,
206
                    splits,
207
                });
208
            }
209
        }
210
    }
211

            
212
    transactions.sort_by(|a, b| b.date.cmp(&a.date));
213

            
214
    Ok(HtmlTemplate(TransactionTableTemplate { transactions }))
215
10
}