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/accounts.rs

//! **ACCOUNTS** (ONE monetize, 5.4) — every (product, tenant) account of the
//! monetize this console speaks to, and one account's rows. Every word is a
//! cell of [`crate::accounts`], tested on the host; this only lays them out.
//! Members never appear: they are the product's, not monetize's.

use leptos::prelude::*;
use leptos_router::hooks::use_query_map;

use crate::accounts::{
    account_api, account_cells, account_href, account_summary_line, payment_cells, row_cells,
    status_cell, AccountDetailJson, AccountsJson, ACCOUNT_COLUMNS, PAYMENT_COLUMNS, ROW_COLUMNS,
};
use crate::ui::{resolved, Badge, Empty, PageHeader, Panel};

/// `/accounts`: product | tenant | rows | Stripe | status, one line per account.
#[component]
pub fn Accounts() -> impl IntoView {
    crate::api::set_estate(None);
    let list =
        LocalResource::new(|| async { crate::api::get::<AccountsJson>("/api/accounts").await });
    view! {
        <main class="page" id="main">
            <PageHeader title="Accounts" subtitle="Every account, by product and customer."/>
            {move || resolved(list.get(), None, |j: AccountsJson| {
                if j.accounts.is_empty() {
                    return view! { <Empty title="No accounts" detail="No accounts yet."/> }.into_any();
                }
                let vendors = j.vendors.clone();
                view! {
                    <Panel title="Accounts">
                        <table class="accounts" aria-label="Accounts">
                            <thead><tr>
                                {ACCOUNT_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}
                            </tr></thead>
                            <tbody>
                                {j.accounts.into_iter().map(|a| {
                                    let [product, tenant, rows, stripe, _] = account_cells(&a, &vendors);
                                    let (tone, status) = status_cell(&a);
                                    let href = account_href(&a.product, &a.tenant);
                                    view! {
                                        <tr>
                                            <td class="mono">{product}</td>
                                            <td class="mono"><a href=href>{tenant}</a></td>
                                            <td>{rows}</td>
                                            <td class="mono">{stripe}</td>
                                            <td><Badge tone=tone label=status/></td>
                                        </tr>
                                    }
                                }).collect_view()}
                            </tbody>
                        </table>
                    </Panel>
                }
                .into_any()
            })}
        </main>
    }
}

/// `/account?product=…&tenant=…`: the account's rows (kind | plan / stop |
/// host | last heard | billing | since), then its payments.
#[component]
pub fn AccountDetail() -> impl IntoView {
    crate::api::set_estate(None);
    let query = use_query_map();
    let key = move || {
        query.with(|q| {
            (
                q.get("product").unwrap_or_default(),
                q.get("tenant").unwrap_or_default(),
            )
        })
    };
    let again = RwSignal::new(0u32);
    let reload = Callback::new(move |()| again.update(|n| *n += 1));
    let detail = LocalResource::new(move || {
        again.get();
        let (product, tenant) = key();
        async move { crate::api::get::<AccountDetailJson>(&account_api(&product, &tenant)).await }
    });
    view! {
        <main class="page" id="main">
            {move || {
                let (product, tenant) = key();
                view! { <PageHeader title=format!("{product} / {tenant}") subtitle="One account: its rows and its payments."/> }
            }}
            <p><a href="/accounts">"← Accounts"</a></p>
            {move || resolved(detail.get(), None, |j: AccountDetailJson| {
                view! {
                    <div class="stack">
                        <p class="hint">{account_summary_line(&j.account)}</p>
                        <Panel title="Rows">
                            <table class="account-rows" aria-label="Rows">
                                <thead><tr>
                                    {ROW_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}
                                </tr></thead>
                                <tbody>
                                    {j.rows.iter().map(|r| {
                                        // ★ UW-3: an owed or waiting private order carries the operator's settles,
                                        // the server's (A32-29): this page draws them.
                                        let settles = r.actions.clone();
                                        let path = crate::accounts::settle_api(&r.id);
                                        let instance = (r.kind == crate::accounts::KIND_PRIVATE).then(|| r.id.clone());
                                        view! {
                                            <tr title=r.words.clone()>
                                                {row_cells(r, crate::pages::now_ms()).into_iter().map(|c| view! { <td>{c}</td> }).collect_view()}
                                            </tr>
                                            {(!settles.is_empty()).then(|| view! {
                                                <tr class="row-actions"><td colspan=ROW_COLUMNS.len().to_string()>
                                                    {settles.into_iter().map(|a| view! {
                                                        <crate::pages::money::MoneyAction label=a.label confirm_label=a.confirm path=path.clone() base=serde_json::json!({ "action": a.verb }) on_done=reload/>
                                                    }).collect_view()}
                                                </td></tr>
                                            })}
                                            {instance.map(|company| view! {
                                                <tr class="row-actions"><td colspan=ROW_COLUMNS.len().to_string()><SetupLog company=company/></td></tr>
                                            })}
                                        }
                                    }).collect_view()}
                                </tbody>
                            </table>
                        </Panel>
                        <Panel title="Payments">
                            {if j.payments.is_empty() {
                                view! { <p class="hint">"No payments on this account."</p> }.into_any()
                            } else {
                                view! {
                                    <table class="account-payments" aria-label="Payments">
                                        <thead><tr>
                                            {PAYMENT_COLUMNS.iter().map(|c| view! { <th scope="col">{*c}</th> }).collect_view()}
                                        </tr></thead>
                                        <tbody>
                                            {j.payments.iter().map(|p| view! {
                                                <tr>{payment_cells(p).into_iter().map(|c| view! { <td>{c}</td> }).collect_view()}</tr>
                                            }).collect_view()}
                                        </tbody>
                                    </table>
                                }.into_any()
                            }}
                        </Panel>
                    </div>
                }
                .into_any()
            })}
        </main>
    }
}

/// ★ UW-13: **a private instance's setup, as narrated** — the order's phases and the
/// installer's lines, read when opened.
#[component]
fn SetupLog(company: String) -> impl IntoView {
    let open = RwSignal::new(false);
    let path = crate::accounts::instance_log_api(&company);
    let log = LocalResource::new(move || {
        let (path, open) = (path.clone(), open.get());
        async move {
            if !open {
                return None;
            }
            Some(crate::api::get::<serde_json::Value>(&path).await)
        }
    });
    view! {
        <button class="btn quiet" on:click=move |_| open.update(|o| *o = !*o)>{move || if open.get() { "Hide the setup log" } else { "Setup log" }}</button>
        {move || match log.get().flatten() {
            None => None,
            Some(Err(f)) => Some(view! { <crate::ui::FailBox fail=f/> }.into_any()),
            Some(Ok(v)) => {
                let lines: Vec<String> = v["lines"].as_array().map(|a| a.iter().map(crate::accounts::log_line).collect()).unwrap_or_default();
                Some(if lines.is_empty() {
                    view! { <p class="hint">"No details."</p> }.into_any()
                } else {
                    view! { <pre class="setup-log mono">{lines.join("\n")}</pre> }.into_any()
                })
            }
        }}
    }
}