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>
}
})
}
}