Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-ui-web/src/pages/money.rs

//! ★ UW-3: **the operator's money fixes, as buttons** — refund or hand-settle a payment
//! owed back, settle an owed private order. Thin: every press is one POST to the console's
//! own server (`monetize_ui_server::money`), armed, then confirmed; nothing is decided here.

use leptos::prelude::*;
use serde_json::{json, Value};

use crate::api::Fail;
use crate::pages::SignupJson;
use crate::ui::Panel;

/// **One money action**: press arms it (and asks for the words it records, when it takes
/// some), the second press confirms and posts `base` + `{<words_field>: …, confirm: true}`.
#[component]
pub fn MoneyAction(
    #[prop(into)] label: String,
    #[prop(into)] confirm_label: String,
    path: String,
    base: Value,
    /// The field the operator's words go in (`reason`, `note`), required; `None` takes none.
    #[prop(optional)]
    words_field: Option<&'static str>,
    on_done: Callback<()>,
) -> impl IntoView {
    let armed = RwSignal::new(false);
    let busy = RwSignal::new(false);
    let words = RwSignal::new(String::new());
    let failed = RwSignal::new(None::<Fail>);
    let done = RwSignal::new(None::<String>);
    let submit = Callback::new(move |()| {
        if busy.get_untracked() {
            return;
        }
        let mut body = base.clone();
        if let Some(field) = words_field {
            let w = words.get_untracked().trim().to_owned();
            if w.is_empty() {
                failed.set(Some(Fail {
                    status: 0,
                    code: String::new(),
                    message: format!("say the {field} first: it is recorded with the money"),
                }));
                return;
            }
            body[field] = Value::String(w);
        }
        body["confirm"] = json!(true);
        busy.set(true);
        failed.set(None);
        let path = path.clone();
        leptos::task::spawn_local(async move {
            match crate::api::post::<_, Value>(&path, &body).await {
                Ok(r) => {
                    let said = r["words"]
                        .as_str()
                        .filter(|w| !w.is_empty())
                        .map(str::to_owned)
                        .unwrap_or_else(|| format!("{}", r["state"].as_str().unwrap_or("done")));
                    done.set(Some(said));
                    armed.set(false);
                    on_done.run(());
                }
                Err(f) => failed.set(Some(f)),
            }
            busy.set(false);
        });
    });
    view! {
        <span class="money-action">
            {move || if armed.get() {
                view! {
                    {words_field.map(|f| view! {
                        <input type="text" placeholder=f prop:value=move || words.get() on:input=move |ev| words.set(event_target_value(&ev))/>
                    })}
                    <button class="btn danger" disabled=move || busy.get() on:click=move |_| submit.run(())>{confirm_label.clone()}</button>
                    <button class="btn quiet" on:click=move |_| armed.set(false)>"Cancel"</button>
                }.into_any()
            } else {
                view! { <button class="btn" on:click=move |_| { done.set(None); armed.set(true) }>{label.clone()}</button> }.into_any()
            }}
            {move || done.get().map(|m| view! { <span class="notice">{m}</span> })}
            // A refusal in plain words: the server's sentence, never a status or a wire code.
            {move || failed.get().map(|f| view! { <div class="failure" role="alert"><p>{crate::sites::fail_words(f.status, &f.message)}</p></div> })}
        </span>
    }
}

/// **OWED BACK**: every payment taken for an account never made, oldest first, each with
/// its two fixes — refund it through the vendor, or close it out as refunded by hand.
/// Draws nothing when nobody is owed anything.
#[component]
pub fn OwedBack() -> impl IntoView {
    let feed = crate::feed::use_feed();
    let again = RwSignal::new(0u32);
    let data = LocalResource::new(move || {
        feed.reread.get();
        again.get();
        async move { crate::api::get::<Value>(&crate::api::site_path("/api/owed-back")).await }
    });
    let reread = Callback::new(move |()| again.update(|n| *n += 1));
    move || {
        let read = data.get().and_then(|r| r.ok()).unwrap_or_default();
        let rows: Vec<SignupJson> = read["signups"]
            .as_array()
            .map(|a| {
                a.iter()
                    .filter_map(|v| serde_json::from_value(v.clone()).ok())
                    .collect()
            })
            .unwrap_or_default();
        (!rows.is_empty()).then(|| {
            view! {
                <Panel title=format!("Owed back · {}", rows.len())>
                    <p class="hint">"Paid, no account made."</p>
                    <div class="sale-list">
                        {rows.into_iter().map(|s| {
                            let handle = format!("{}:{}", s.vendor, s.session);
                            let base = json!({ "vendor": s.vendor, "session": s.session });
                            view! {
                                <div class="sale-row">
                                    <span class="what" title=handle.clone()>{crate::fmt::signup_line(&s)}</span>
                                    <span class="price mono hint">{handle}</span>
                                    <span class="mark">
                                        <MoneyAction label="Refund…" confirm_label="Confirm: refund the whole payment" path="/api/signups/refund".to_owned() base=base.clone() words_field="reason" on_done=reread/>
                                        <MoneyAction label="Refunded by hand…" confirm_label="Confirm: close it out" path="/api/signups/settle".to_owned() base=base words_field="note" on_done=reread/>
                                    </span>
                                </div>
                            }
                        }).collect_view()}
                    </div>
                </Panel>
            }
        })
    }
}