peillute/views/
actions.rs

1//! Transaction action components for the Peillute application
2//!
3//! This module provides components for various financial transactions in the system,
4//! including viewing transaction history, making deposits, withdrawals, payments,
5//! refunds, and transfers between users.
6
7use dioxus::prelude::*;
8
9// show all transactions as vertical card list
10/// Transaction history component
11///
12/// Displays a list of all transactions for a specific user, showing details such as
13/// the source and destination users, amount, and any associated messages.
14#[component]
15pub fn History(name: String) -> Element {
16    let name = std::rc::Rc::new(name);
17    let name_for_future = name.clone();
18
19    let transactions_resource = use_resource(move || {
20        let name_clone = name_for_future.clone();
21        async move { get_transactions_for_user_server(name_clone.to_string()).await }
22    });
23
24    rsx! {
25        div { id: "history-page",
26            match &*transactions_resource.read() {
27                None => rsx! {
28                    p { "Loading history..." }
29                },
30                Some(Ok(transactions)) => {
31                    if transactions.is_empty() {
32                        rsx! {
33                            p { "No transactions found for {name}." }
34                        }
35                    } else {
36                        rsx! {
37                            ul { class: "transactions-list",
38                                for transaction in transactions.iter() {
39                                    li {
40                                        key: "{transaction.lamport_time}-{transaction.source_node}",
41                                        class: "transaction-card",
42                                        p {
43                                            strong { "From:" }
44                                            " {transaction.from_user}"
45                                        }
46                                        p {
47                                            strong { "To:" }
48                                            " {transaction.to_user}"
49                                        }
50                                        p {
51                                            strong { "Amount:" }
52                                            " {transaction.amount:.2}"
53                                        }
54                                        if let Some(msg) = &transaction.optional_msg {
55                                            if !msg.is_empty() {
56                                                p {
57                                                    strong { "Message:" }
58                                                    " {msg}"
59                                                }
60                                            }
61                                        }
62                                    }
63                                }
64                            }
65                        }
66                    }
67                }
68                Some(Err(e)) => rsx! {
69                    p { class: "error-message", "Error loading history: {e}" }
70                },
71            }
72        }
73    }
74}
75
76// take the username and collect the an amount (float from form) to make a withdrawal
77/// Withdrawal component
78///
79/// Provides a form for users to withdraw money from their account, with input
80/// validation to ensure positive amounts and sufficient funds.
81#[component]
82pub fn Withdraw(name: String) -> Element {
83    let mut withdraw_amount = use_signal(|| 0f64);
84    let name = std::rc::Rc::new(name);
85
86    let mut error_signal = use_signal(|| None::<String>);
87
88    let name_for_future = name.clone();
89
90    rsx! {
91        div { id: "withdraw-form",
92            form {
93                label { r#for: "fwithdraw", "Withdraw amount :" }
94                input {
95                    r#type: "number",
96                    id: "form-withdraw",
97                    r#name: "fwithdraw",
98                    step: 0.01,
99                    value: "{withdraw_amount}",
100                    oninput: move |event| {
101                        if let Ok(as_number) = event.value().parse::<f64>() {
102                            withdraw_amount.set(as_number);
103                        }
104                    },
105                }
106                button {
107                    r#type: "submit",
108                    onclick: move |_| {
109                        let name = name_for_future.clone();
110                        let amount = *withdraw_amount.read();
111                        async move {
112                            if amount >= 0.0 {
113                                if let Ok(_) = withdraw_for_user_server(name.to_string(), amount).await {
114                                    withdraw_amount.set(0.0);
115                                    error_signal.set(None);
116                                }
117                            } else {
118                                error_signal
119                                    .set(
120                                        Some(
121                                            format!("Please enter a positive amount, you gave {amount}."),
122                                        ),
123                                    );
124                            }
125                        }
126                    },
127                    "Submit"
128                }
129            }
130            if let Some(error) = &*error_signal.read() {
131                p { class: "error-message", "{error}" }
132            }
133        }
134    }
135}
136
137const COCA_IMG: Asset = asset!("/assets/images/coca.png");
138const CHIPS_IMG: Asset = asset!("/assets/images/chips.png");
139const SANDWICH_IMG: Asset = asset!("/assets/images/sandwich.png");
140const COFFEE_IMG: Asset = asset!("/assets/images/coffee.png");
141
142const PRODUCTS: &[(&str, f64, Asset)] = &[
143    ("Coca", 1.50, COCA_IMG),
144    ("Chips", 2.00, CHIPS_IMG),
145    ("Sandwich", 4.50, SANDWICH_IMG),
146    ("Coffee", 1.20, COFFEE_IMG),
147];
148
149// take the username and collect the an amount (float from form) to make a payment
150/// Payment component
151///
152/// Implements a product catalog interface where users can select items to purchase,
153/// with a running total and order summary. Supports multiple products with
154/// individual quantity selection.
155#[component]
156pub fn Pay(name: String) -> Element {
157    let mut product_quantities = use_signal(|| vec![0u32; PRODUCTS.len()]);
158    let name_for_payment = std::rc::Rc::new(name.clone());
159
160    let mut error_signal = use_signal(|| None::<String>);
161
162    let handle_pay = move |_| {
163        let current_quantities = product_quantities.read().clone();
164        let name_clone = name_for_payment.clone();
165
166        let mut total_amount = 0.0;
167        for (i, &(_, price, _)) in PRODUCTS.iter().enumerate() {
168            if let Some(&quantity) = current_quantities.get(i) {
169                total_amount += price * quantity as f64;
170            }
171        }
172
173        spawn(async move {
174            if total_amount > 0.0 {
175                if let Ok(_) = pay_for_user_server(name_clone.to_string(), total_amount).await {
176                    log::info!("Payment successful.");
177                    product_quantities.set(vec![0u32; PRODUCTS.len()]);
178                    error_signal.set(None);
179                }
180            } else {
181                log::warn!("Attempted to pay with a total of 0.0. No action taken.");
182                error_signal.set(Some(
183                    "Cannot pay €0. Please select at least one item.".to_string(),
184                ));
185            }
186        });
187    };
188
189    let current_total_display = use_memo(move || {
190        let mut total = 0.0;
191        let quantities_read = product_quantities.read();
192        for (i, &(_, price, _)) in PRODUCTS.iter().enumerate() {
193            if let Some(&quantity) = quantities_read.get(i) {
194                total += price * quantity as f64;
195            }
196        }
197        total
198    });
199
200    rsx! {
201        div { id: "pay-page",
202            div {
203                for (index , (product_name , price , image_path)) in PRODUCTS.iter().enumerate() {
204                    div { key: "{product_name}-{index}",
205                        img { src: "{image_path}", alt: "{product_name}" }
206                        div { class: "product-info",
207                            h3 { "{product_name}" }
208                            p { "€{price:.2}" }
209                            div {
210                                label { r#for: "qty-{index}", "Quantity:" }
211                                input {
212                                    r#type: "number",
213                                    id: "qty-{index}",
214                                    min: "0",
215                                    value: "{product_quantities.read()[index]}",
216                                    oninput: move |event| {
217                                        let mut pq_signal_for_input = product_quantities;
218                                        if let Ok(new_quantity) = event.value().parse::<u32>() {
219                                            let mut quantities_writer = pq_signal_for_input.write();
220                                            if index < quantities_writer.len() {
221                                                quantities_writer[index] = new_quantity;
222                                            }
223                                        } else if event.value().is_empty() {
224                                            let mut quantities_writer = pq_signal_for_input.write();
225                                            if index < quantities_writer.len() {
226                                                quantities_writer[index] = 0;
227                                            }
228                                        }
229                                    },
230                                }
231                            }
232                        }
233                    }
234                }
235            }
236
237            div { class: "cart-summary",
238                h2 { "Order Summary" }
239                h3 { "Total: €{current_total_display():.2}" }
240                form {
241                    button {
242                        r#type: "submit",
243                        disabled: current_total_display() == 0.0,
244                        onclick: handle_pay,
245                        "Pay Now"
246                    }
247                }
248            }
249
250            if let Some(error) = &*error_signal.read() {
251                p { class: "error-message", "{error}" }
252            }
253        }
254    }
255}
256
257// show all transactions as vertical card list
258// allow the user to select a transaction to refund it
259/// Refund component
260///
261/// Displays a list of transactions that can be refunded, allowing users to
262/// reverse previous transactions. Shows transaction details and provides
263/// refund functionality.
264#[component]
265pub fn Refund(name: String) -> Element {
266    let name = std::rc::Rc::new(name);
267    let name_for_future = name.clone();
268
269    let mut error_signal = use_signal(|| None::<String>);
270
271    let transactions_resource = use_resource(move || {
272        let name_clone = name_for_future.clone();
273        async move { get_transactions_for_user_server(name_clone.to_string()).await }
274    });
275
276    rsx! {
277        div { id: "refund-page",
278            match &*transactions_resource.read() {
279                None => rsx! {
280                    p { "Loading history..." }
281                },
282                Some(Ok(transactions)) => {
283                    let name_clone = name.clone();
284                    if transactions.is_empty() {
285                        rsx! {
286                            p { "No transactions found for {name_clone}." }
287                        }
288                    } else {
289                        rsx! {
290                            ul { class: "transactions-list",
291                                for transaction in transactions.iter() {
292                                    li {
293                                        key: "{transaction.lamport_time}-{transaction.source_node}",
294                                        class: "transaction-card",
295                                        p {
296                                            strong { "From:" }
297                                            " {transaction.from_user}"
298                                        }
299                                        p {
300                                            strong { "To:" }
301                                            " {transaction.to_user}"
302                                        }
303                                        p {
304                                            strong { "Amount:" }
305                                            " {transaction.amount:.2}"
306                                        }
307                                        if let Some(msg) = &transaction.optional_msg {
308                                            if !msg.is_empty() {
309                                                p {
310                                                    strong { "Message:" }
311                                                    " {msg}"
312                                                }
313                                            }
314                                        }
315                                        {
316                                            let transaction_for_refund = transaction.clone();
317                                            let name_for_refund = name.clone();
318                                            let mut resource_to_refresh = transactions_resource.clone();
319                                            rsx! {
320                                                button {
321                                                    r#type: "submit",
322                                                    onclick: move |_| {
323                                                        let name_for_future = name_for_refund.clone();
324                                                        let transaction_for_future = transaction_for_refund.clone();
325                                                        async move {
326                                                            if let Ok(_) = refund_transaction_server(
327                                                                    name_for_future.to_string(),
328                                                                    transaction_for_future.lamport_time,
329                                                                    transaction_for_future.source_node,
330                                                                )
331                                                                .await
332                                                            {
333                                                                if let Ok(_) = get_transactions_for_user_server(
334                                                                        name_for_future.to_string(),
335                                                                    )
336                                                                    .await
337                                                                {
338                                                                    error_signal.set(None);
339                                                                    resource_to_refresh.restart();
340                                                                }
341                                                            }
342                                                        }
343                                                    },
344                                                    "Refund"
345                                                }
346                                            }
347                                        }
348                                    }
349                                }
350                            }
351                            if let Some(error) = &*error_signal.read() {
352                                p { class: "error-message", "{error}" }
353                            }
354                        }
355                    }
356                }
357                Some(Err(e)) => rsx! {
358                    p { class: "error-message", "Error loading transactions: {e}" }
359                },
360            }
361        }
362    }
363}
364
365// allow to select a user between all users (except the current one)
366// and allow user to transfer money with an amout (float from form) to another user
367// allow user to add a message to the transaction
368/// Transfer component
369///
370/// Enables users to transfer money to other users in the system, with features for:
371/// - Selecting the recipient from a list of available users
372/// - Specifying the transfer amount
373/// - Adding an optional message to the transaction
374/// - Generating random messages for fun
375#[component]
376pub fn Transfer(name: String) -> Element {
377    let mut transfer_amount = use_signal(|| 0f64);
378    let mut transfer_message = use_signal(String::new);
379    let mut selected_user = use_signal(String::new);
380    let name = std::rc::Rc::new(name);
381    let name_for_future = name.clone();
382
383    let mut error_signal = use_signal(|| None::<String>);
384
385    let users_resource = use_resource({
386        move || {
387            let current_user = name_for_future.clone();
388            async move {
389                let all_users = get_users_server().await.unwrap_or_default();
390                all_users
391                    .into_iter()
392                    .filter(|u| u != current_user.as_ref())
393                    .collect::<Vec<_>>()
394            }
395        }
396    });
397
398    rsx! {
399        div { id: "transfer-page",
400            match &*users_resource.read() {
401                None => rsx! {
402                    p { "Loading users..." }
403                },
404                Some(users) => rsx! {
405                    form {
406                        label { r#for: "user-select", "Select user to transfer to:" }
407                        select {
408                            id: "user-select",
409                            onchange: move |evt| {
410                                selected_user.set(evt.value());
411                            },
412                            option {
413                                value: "",
414                                disabled: true,
415                                selected: selected_user.read().is_empty(),
416                                "Choose a user"
417                            }
418                            for user in users {
419                                option { key: "{user}", value: "{user}", "{user}" }
420                            }
421                        }
422                        label { r#for: "transfer-amount", "Amount to transfer:" }
423                        input {
424                            r#type: "number",
425                            id: "transfer-amount",
426                            step: 0.01,
427                            value: "{transfer_amount}",
428                            oninput: move |evt| {
429                                if let Ok(val) = evt.value().parse::<f64>() {
430                                    transfer_amount.set(val);
431                                }
432                            },
433                        }
434                        label { r#for: "transfer-message", "Message (optional):" }
435                        input {
436                            r#type: "text",
437                            id: "transfer-message",
438                            value: "{transfer_message}",
439                            oninput: move |evt| {
440                                transfer_message.set(evt.value());
441                            },
442                        }
443                        button {
444                            r#type: "button",
445                            onclick: move |_| {
446                                async move {
447                                    if let Ok(message) = get_random_message_server().await {
448                                        transfer_message.set(message);
449                                    }
450                                }
451                            },
452                            "Select a random message"
453                        }
454                        button {
455                            r#type: "submit",
456                            onclick: move |_| {
457                                let to_user = selected_user.read().clone();
458                                let amount = *transfer_amount.read();
459                                let message = transfer_message.read().clone();
460                                let from_user = name.clone();
461                                async move {
462                                    if !to_user.is_empty() && amount > 0.0 {
463                                        if let Ok(_) = transfer_from_user_to_user_server(
464                                                from_user.to_string(),
465                                                to_user,
466                                                amount,
467                                                message,
468                                            )
469                                            .await
470                                        {
471                                            transfer_amount.set(0.0);
472                                            transfer_message.set(String::new());
473                                            selected_user.set(String::new());
474                                            error_signal.set(None);
475                                        }
476                                    } else {
477                                        error_signal
478                                            .set(
479                                                Some(
480                                                    "Please select a user and enter a positive amount."
481                                                        .to_string(),
482                                                ),
483                                            );
484                                    }
485                                }
486                            },
487                            "Transfer"
488                        }
489                    }
490                },
491            }
492            if let Some(error) = &*error_signal.read() {
493                p { class: "error-message", "{error}" }
494            }
495        }
496    }
497}
498
499// take the username and collect the an amount (float from form) to make a deposit
500/// Deposit component
501///
502/// Provides a form for users to deposit money into their account, with input
503/// validation to ensure positive amounts.
504#[component]
505pub fn Deposit(name: String) -> Element {
506    let mut deposit_amount = use_signal(|| 0f64);
507    let name = std::rc::Rc::new(name);
508
509    let mut error_signal = use_signal(|| None::<String>);
510
511    let name_for_future = name.clone();
512
513    rsx! {
514        div { id: "deposit-form",
515            form {
516                label { r#for: "fdeposit", "Deposit amount :" }
517                input {
518                    r#type: "number",
519                    id: "form-deposit",
520                    r#name: "fdeposit",
521                    step: 0.01,
522                    value: "{deposit_amount}",
523                    oninput: move |event| {
524                        if let Ok(as_number) = event.value().parse::<f64>() {
525                            deposit_amount.set(as_number);
526                        }
527                    },
528                }
529                button {
530                    r#type: "submit",
531                    onclick: move |_| {
532                        let name = name_for_future.clone();
533                        let amount = *deposit_amount.read();
534                        async move {
535                            if amount >= 0.0 {
536                                if let Ok(_) = deposit_for_user_server(name.to_string(), amount).await {
537                                    deposit_amount.set(0.0);
538                                    error_signal.set(None);
539                                }
540                            } else {
541                                error_signal
542                                    .set(
543                                        Some(
544                                            format!("Please enter a positive amount, you gave {amount}."),
545                                        ),
546                                    );
547                            }
548                        }
549                    },
550                    "Submit"
551                }
552            }
553            if let Some(error) = &*error_signal.read() {
554                p { class: "error-message", "{error}" }
555            }
556        }
557    }
558}
559
560#[cfg(feature = "server")]
561const RANDOM_MESSAGE: &[&str] = &[
562    "Prend tes 200 balles et va te payer des cours de theatre",
563    "C'est pour toi bb",
564    "Love sur toi",
565    "Phrase non aléatoire",
566    "Votre argent messire",
567    "Acompte sur livraison cocaine",
568    "Votre argent seigneur",
569    "Pour tout ce que tu fais pour moi",
570    "Remboursement horny.com",
571    "Puta, où tu étais quand j'mettais des sept euros d'essence",
572    "Parce que l'argent n'est pas un problème pour moi",
573    "Tiens le rat",
574    "Pour le rein",
575    "Abonnement OnlyFans",
576    "Pour notre dernière nuit, pourboire non compris",
577    "ça fait beaucoup la non ?",
578    "Chantage SexTape",
579    "Argent sale",
580    "Adhésion front national",
581    "Ce que tu sais...",
582    "Remboursement dot de ta soeur",
583    "Rien à ajouter",
584    "Téléphone rose",
585    "Raison : \"GnaGnaGna moi je paye pas pour vous\"",
586    "Fond de tiroir",
587    "Epilation des zones intimes",
588    "Pour m'avoir gratouillé le dos",
589    "La reine Babeth vous offre cet argent",
590    "Nan t'inquiete",
591];
592
593#[cfg(feature = "server")]
594fn get_seed() -> u64 {
595    use std::time::{SystemTime, UNIX_EPOCH};
596    SystemTime::now()
597        .duration_since(UNIX_EPOCH)
598        .unwrap()
599        .as_nanos() as u64
600}
601
602#[cfg(feature = "server")]
603fn lcg(seed: u64) -> u64 {
604    const A: u64 = 6364136223846793005;
605    const C: u64 = 1;
606    seed.wrapping_mul(A).wrapping_add(C)
607}
608
609#[server]
610async fn get_random_message_server() -> Result<String, ServerFnError> {
611    let seed = get_seed();
612    let random_number = lcg(seed);
613    let message = RANDOM_MESSAGE[random_number as usize % RANDOM_MESSAGE.len()];
614    Ok(message.to_string())
615}
616
617#[server]
618async fn get_users_server() -> Result<Vec<String>, ServerFnError> {
619    use crate::db;
620    let users = db::get_users()?;
621    Ok(users)
622}
623
624#[server]
625async fn deposit_for_user_server(user: String, amount: f64) -> Result<(), ServerFnError> {
626    if amount < 0.0 {
627        return Err(ServerFnError::new("Amount cannot be negative."));
628    }
629
630    if let Err(e) = crate::control::enqueue_critical(crate::control::CriticalCommands::Deposit {
631        name: user,
632        amount: amount,
633    })
634    .await
635    {
636        return Err(ServerFnError::new(format!(
637            "[SERVER] Failed to diffuse deposit : {e}"
638        )));
639    }
640
641    Ok(())
642}
643
644#[server]
645async fn withdraw_for_user_server(user: String, amount: f64) -> Result<(), ServerFnError> {
646    if amount < 0.0 {
647        return Err(ServerFnError::new("Amount cannot be negative."));
648    }
649
650    if let Err(e) = crate::control::enqueue_critical(crate::control::CriticalCommands::Withdraw {
651        name: user,
652        amount: amount,
653    })
654    .await
655    {
656        return Err(ServerFnError::new(format!(
657            "[SERVER] Failed to withdraw : {e}"
658        )));
659    }
660
661    Ok(())
662}
663
664#[server]
665async fn pay_for_user_server(user: String, amount: f64) -> Result<(), ServerFnError> {
666    if amount < 0.0 {
667        return Err(ServerFnError::new("Amount cannot be negative."));
668    }
669
670    if let Err(e) = crate::control::enqueue_critical(crate::control::CriticalCommands::Pay {
671        name: user,
672        amount: amount,
673    })
674    .await
675    {
676        return Err(ServerFnError::new(format!("[SERVER] Failed to pay : {e}")));
677    }
678
679    Ok(())
680}
681
682#[server]
683async fn transfer_from_user_to_user_server(
684    from_user: String,
685    to_user: String,
686    amount: f64,
687    _optional_message: String,
688) -> Result<(), ServerFnError> {
689    if amount < 0.0 {
690        return Err(ServerFnError::new("Amount cannot be negative."));
691    }
692
693    if let Err(e) = crate::control::enqueue_critical(crate::control::CriticalCommands::Transfer {
694        from: from_user,
695        to: to_user,
696        amount: amount,
697    })
698    .await
699    {
700        return Err(ServerFnError::new(format!(
701            "[SERVER] Failed to make the transfer: {e}"
702        )));
703    }
704
705    Ok(())
706}
707
708#[server]
709async fn get_transactions_for_user_server(
710    name: String,
711) -> Result<Vec<crate::db::Transaction>, ServerFnError> {
712    if let Ok(data) = crate::db::get_transactions_for_user(&name) {
713        Ok(data)
714    } else {
715        Err(ServerFnError::new("User not found."))
716    }
717}
718
719#[server]
720async fn refund_transaction_server(
721    name: String,
722    lamport_time: i64,
723    transac_node: String,
724) -> Result<(), ServerFnError> {
725    if let Err(e) = crate::control::enqueue_critical(crate::control::CriticalCommands::Refund {
726        name: name,
727        lamport: lamport_time,
728        node: transac_node,
729    })
730    .await
731    {
732        return Err(ServerFnError::new(format!(
733            "[SERVER] Failed to refund : {e}"
734        )));
735    }
736
737    Ok(())
738}