//! Super dashboard โ€” one screen for everything you hold in Cyberia.
//! Soft3 local: profile, CX/ETH balance, robots, leases, events, cities, intents.

use crate::cities::{load_found, CityCard};
use crate::economy::{fmt_qty, good};
use crate::events::{load_user_events, EventCard};
use crate::land::FLAG_SVG;
use crate::nav::CyberiaNav;
use crate::robots::{load_owned, OwnedRobot};
use crate::wallet::{
    ensure_economy_boot, load_balance, load_intents, load_leases, load_ledger, load_orders,
    load_profile,
    load_stocks, save_balance, save_profile, IntentRec, Lease, SoftBalance, StockLine,
};
use leptos::prelude::*;

fn fmt_cx(v: f64) -> String {
    if v >= 1000.0 {
        format!("{v:.0}")
    } else if v >= 10.0 {
        format!("{v:.1}")
    } else {
        format!("{v:.2}")
    }
}

fn fmt_eth(v: f64) -> String {
    if v >= 10.0 {
        format!("{v:.2}")
    } else {
        format!("{v:.3}")
    }
}

fn activity_score(
    robots: usize,
    leases: usize,
    events: usize,
    cities: usize,
    intents: usize,
    depth: f64,
) -> f64 {
    let raw = robots as f64 * 8.0
        + leases as f64 * 12.0
        + events as f64 * 6.0
        + cities as f64 * 15.0
        + intents as f64 * 2.0
        + depth * 0.4;
    raw.clamp(0.0, 99.0)
}

#[component]
pub fn MePage() -> impl IntoView {
    let profile = RwSignal::new(load_profile());
    let balance = RwSignal::new(load_balance());
    let robots = RwSignal::new(load_owned());
    let leases = RwSignal::new(load_leases());
    let events = RwSignal::new(load_user_events());
    let cities = RwSignal::new(load_found());
    let intents = RwSignal::new(load_intents());
    let stocks = RwSignal::new(load_stocks());
    let orders = RwSignal::new(load_orders());

    let edit_open = RwSignal::new(false);
    let draft_handle = RwSignal::new(String::new());
    let draft_name = RwSignal::new(String::new());

    // refresh from storage when landing (other tabs / map session)
    Effect::new(move |_| {
        document().set_title("Cyberia โ€” you");
        ensure_economy_boot();
        profile.set(load_profile());
        balance.set(load_balance());
        robots.set(load_owned());
        leases.set(load_leases());
        events.set(load_user_events());
        cities.set(load_found());
        intents.set(load_intents());
        stocks.set(load_stocks());
        orders.set(load_orders());
    });

    let score = move || {
        activity_score(
            robots.get().len(),
            leases.get().len(),
            events.get().len(),
            cities.get().len(),
            intents.get().len(),
            balance.get().depth,
        )
    };

    view! {
        <div class="page-shell cities-shell">
            <div class="site-chrome cyberia-chrome">
                <div class="chrome-inner">
                    <div class="header-row1">
                        <div class="logo-zone">
                            <h1 class="logo">
                                <a href="/cities" class="brand-flag" title="home" inner_html=FLAG_SVG></a>
                                <span style="color: var(--cyber-green);">"cyber"</span>
                                <span style="color: var(--cyber-green); margin: 0 1px;">"โ€ข"</span>
                                <span style="color: #fff;">"ia"</span>
                            </h1>
                        </div>
                        <div class="cyberia-phase-pill">
                            <span class="phase-dot"></span>
                            {move || format!("@{} ยท YOU", profile.get().handle)}
                        </div>
                        <CyberiaNav active="you" />
                    </div>
                </div>
            </div>

            <div class="cities-stage me-stage">
                <div class="cities-hero">
                    <div>
                        <div class="cities-kicker">"SUPER DASHBOARD ยท THIS BROWSER"</div>
                        <h2 class="cities-title">
                            {move || {
                                let p = profile.get();
                                if p.name.is_empty() || p.name == "Anonymous" {
                                    format!("@{}", p.handle)
                                } else {
                                    p.name
                                }
                            }}
                        </h2>
                        <p class="cities-lead">
                            "Everything you hold in Cyberia โ€” fleet, land, events, cities, intents, soft balance. Soft3 local until the backend ships."
                        </p>
                    </div>
                </div>

                // identity + balance strip
                <div class="me-id-row">
                    <div class="me-id-card">
                        <div class="me-avatar">{move || {
                            let h = profile.get().handle;
                            h.chars().next().unwrap_or('?').to_ascii_uppercase().to_string()
                        }}</div>
                        <div class="me-id-copy">
                            <div class="me-handle">{move || format!("@{}", profile.get().handle)}</div>
                            <div class="me-since">{move || format!("since ยท {}", profile.get().since)}</div>
                        </div>
                        <button class="chip" on:click=move |_| {
                            let p = profile.get();
                            draft_handle.set(p.handle);
                            draft_name.set(p.name);
                            edit_open.set(true);
                        }>"EDIT"</button>
                    </div>
                    <div class="me-bal-card">
                        <div class="kpi-lab">"CX BALANCE"</div>
                        <div class="kpi-val pos">{move || format!("{} CX", fmt_cx(balance.get().cx))}</div>
                        <div class="kpi-sub">"soft century-index units"</div>
                    </div>
                    <div class="me-bal-card">
                        <div class="kpi-lab">"ETH FLOAT"</div>
                        <div class="kpi-val">{move || format!("{} ETH", fmt_eth(balance.get().eth))}</div>
                        <div class="kpi-sub">"mock bank participation"</div>
                    </div>
                    <div class="me-bal-card">
                        <div class="kpi-lab">"ACTIVITY"</div>
                        <div class="kpi-val">{move || format!("{:.0}", score())}</div>
                        <div class="kpi-sub">"depth from ops ยท 0โ€“99"</div>
                    </div>
                </div>

                // summary counts
                <div class="bank-kpi-grid me-counts">
                    <a class="kpi me-kpi" href="/robots">
                        <div class="kpi-lab">"ROBOTS"</div>
                        <div class="kpi-val">{move || robots.get().len().to_string()}</div>
                        <div class="kpi-sub">"owned fleet โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/map">
                        <div class="kpi-lab">"LEASES"</div>
                        <div class="kpi-val">{move || leases.get().len().to_string()}</div>
                        <div class="kpi-sub">"plots you hold โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/calendar">
                        <div class="kpi-lab">"EVENTS"</div>
                        <div class="kpi-val">{move || events.get().len().to_string()}</div>
                        <div class="kpi-sub">"you posted โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/cities">
                        <div class="kpi-lab">"CITIES"</div>
                        <div class="kpi-val">{move || cities.get().len().to_string()}</div>
                        <div class="kpi-sub">"you founded โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/map">
                        <div class="kpi-lab">"INTENTS"</div>
                        <div class="kpi-val">{move || intents.get().len().to_string()}</div>
                        <div class="kpi-sub">"ops queue โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/products">
                        <div class="kpi-lab">"STOCKS"</div>
                        <div class="kpi-val">{move || stocks.get().len().to_string()}</div>
                        <div class="kpi-sub">"elements ยท products โ†’"</div>
                    </a>
                    <a class="kpi me-kpi" href="/orgs">
                        <div class="kpi-lab">"ORGS"</div>
                        <div class="kpi-val">{move || {
                            use crate::signal::{graph_links, word_particle};
                            let me_w = word_particle("person", &profile.get().handle);
                            let member_of = word_particle("relation", "member_of");
                            graph_links().iter().filter(|(_, l)| l.rel == member_of && l.from == me_w).count().to_string()
                        }}</div>
                        <div class="kpi-sub">"memberships โ†’"</div>
                    </a>
                </div>

                // quick actions
                <div class="me-actions">
                    <a class="cta-btn cta-lease cta-lg" href="/products" style="text-decoration:none;">
                        <span class="cta-copy">
                            <span class="cta-title">"BOM"</span>
                            <span class="cta-sub">"transform stocks"</span>
                        </span>
                    </a>
                    <a class="cta-btn cta-buy cta-lg" href="/services" style="text-decoration:none;">
                        <span class="cta-copy">
                            <span class="cta-title">"SERVICES"</span>
                            <span class="cta-sub">"order the fleet"</span>
                        </span>
                    </a>
                    <a class="cta-btn cta-event cta-lg" href="/robots" style="text-decoration:none;">
                        <span class="cta-copy">
                            <span class="cta-title">"ROBOTS"</span>
                            <span class="cta-sub">"fleet"</span>
                        </span>
                    </a>
                    <a class="cta-btn cta-found cta-lg" href="/world" style="text-decoration:none;">
                        <span class="cta-copy">
                            <span class="cta-title">"WORLD"</span>
                            <span class="cta-sub">"my presence"</span>
                        </span>
                    </a>
                </div>

                // โ”€โ”€ STOCKS โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"YOUR STOCKS ยท ELEMENTS & PRODUCTS"</span>
                        <a href="/products">"BOM โ†’"</a>
                    </div>
                    {move || {
                        let list = stocks.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "Empty book. "
                                    <a href="/market">"Buy on market"</a>
                                    " or wait for starter pack boot."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-chip-grid">
                                {list.into_iter().map(|s: StockLine| {
                                    let name = good(&s.id).map(|g| g.name).unwrap_or("?");
                                    let unit = good(&s.id).map(|g| g.unit).unwrap_or("");
                                    view! {
                                        <a class="me-chip" href="/market">
                                            <span class="me-chip-name">{name}</span>
                                            <span class="me-chip-meta">{format!("{} {} ยท {}", fmt_qty(s.qty), unit, s.id)}</span>
                                        </a>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ P&L โ€” income vs spend by category, from the CX journal โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"P&L ยท BY CATEGORY"</span>
                        <span style="color:#556; font-size:10px;">"from the CX journal"</span>
                    </div>
                    {move || {
                        let _ = intents.get();
                        let ledger = load_ledger();
                        if ledger.is_empty() {
                            return view! {
                                <div class="me-empty">"No CX movements recorded yet โ€” trade an element, order a service, buy a robot."</div>
                            }.into_any();
                        }
                        let mut cats: std::collections::BTreeMap<String, (f64, f64)> = std::collections::BTreeMap::new();
                        for e in &ledger {
                            let slot = cats.entry(e.cat.clone()).or_insert((0.0, 0.0));
                            if e.dir == "in" { slot.0 += e.amount; } else { slot.1 += e.amount; }
                        }
                        let (tin, tout): (f64, f64) = cats.values().fold((0.0, 0.0), |a, v| (a.0 + v.0, a.1 + v.1));
                        let net = tin - tout;
                        view! {
                            <div class="pnl-table">
                                <div class="pnl-row pnl-head">
                                    <span>"CATEGORY"</span>
                                    <span class="pnl-num">"IN"</span>
                                    <span class="pnl-num">"OUT"</span>
                                    <span class="pnl-num">"NET"</span>
                                </div>
                                {cats.into_iter().map(|(cat, (cin, cout))| {
                                    let n = cin - cout;
                                    view! {
                                        <div class="pnl-row">
                                            <span style="text-transform:uppercase; letter-spacing:1px;">{cat}</span>
                                            <span class="pnl-num" style="color:var(--cyber-green);">{if cin > 0.0 { format!("+{}", fmt_qty(cin)) } else { "โ€”".into() }}</span>
                                            <span class="pnl-num" style="color:var(--cyber-red);">{if cout > 0.0 { format!("-{}", fmt_qty(cout)) } else { "โ€”".into() }}</span>
                                            <span class="pnl-num" style=format!("color:{};", if n >= 0.0 { "var(--cyber-green)" } else { "var(--cyber-red)" })>
                                                {format!("{}{}", if n >= 0.0 { "+" } else { "" }, fmt_qty(n))}
                                            </span>
                                        </div>
                                    }
                                }).collect_view()}
                                <div class="pnl-row pnl-total">
                                    <span>"TOTAL"</span>
                                    <span class="pnl-num" style="color:var(--cyber-green);">{format!("+{}", fmt_qty(tin))}</span>
                                    <span class="pnl-num" style="color:var(--cyber-red);">{format!("-{}", fmt_qty(tout))}</span>
                                    <span class="pnl-num" style=format!("font-weight:700; color:{};", if net >= 0.0 { "var(--cyber-green)" } else { "var(--cyber-red)" })>
                                        {format!("{}{}", if net >= 0.0 { "+" } else { "" }, fmt_qty(net))}
                                    </span>
                                </div>
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ CASHFLOW โ€” the journal itself, newest first, running balance โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"CASHFLOW"</span>
                        <span style="color:#556; font-size:10px;">{move || {
                            let _ = intents.get();
                            format!("{} movements ยท balance {} CX", load_ledger().len(), fmt_qty(load_balance().cx))
                        }}</span>
                    </div>
                    {move || {
                        let _ = intents.get();
                        let ledger = load_ledger();
                        if ledger.is_empty() {
                            return view! {
                                <div class="me-empty">"The journal is empty โ€” every CX in or out will land here."</div>
                            }.into_any();
                        }
                        // walk backwards from the live balance to reconstruct running balance
                        let mut bal = load_balance().cx;
                        let mut rows: Vec<(String, String, String, f64, f64)> = Vec::new();
                        for e in ledger.iter().rev().take(25) {
                            let when = {
                                let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(e.t));
                                let iso: String = d.to_iso_string().into();
                                iso.get(5..16).unwrap_or("").replace('T', " ")
                            };
                            let signed = if e.dir == "in" { e.amount } else { -e.amount };
                            rows.push((when, e.cat.clone(), e.note.clone(), signed, bal));
                            bal -= signed;
                        }
                        view! {
                            <div class="pnl-table">
                                <div class="pnl-row pnl-head cash-row">
                                    <span>"WHEN ยท WHAT"</span>
                                    <span class="pnl-num">"FLOW"</span>
                                    <span class="pnl-num">"BALANCE"</span>
                                </div>
                                {rows.into_iter().map(|(when, cat, note, flow, bal)| {
                                    view! {
                                        <div class="pnl-row cash-row">
                                            <span>
                                                <span style="color:#556; font-size:10px; margin-right:8px;">{when}</span>
                                                <span style="color:var(--cyber-cyan); font-size:10px; letter-spacing:1px; margin-right:8px;">{cat.to_uppercase()}</span>
                                                <span style="color:#99a;">{note}</span>
                                            </span>
                                            <span class="pnl-num" style=format!("color:{};", if flow >= 0.0 { "var(--cyber-green)" } else { "var(--cyber-red)" })>
                                                {format!("{}{}", if flow >= 0.0 { "+" } else { "" }, fmt_qty(flow))}
                                            </span>
                                            <span class="pnl-num" style="color:#889;">{fmt_qty(bal)}</span>
                                        </div>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ FLEET โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"YOUR FLEET"</span>
                        <a href="/robots">"all robots โ†’"</a>
                    </div>
                    {move || {
                        let list = robots.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "No robots yet. "
                                    <a href="/robots">"Buy one"</a>
                                    " โ€” soft3 local catalog."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-chip-grid">
                                {list.into_iter().map(|r: OwnedRobot| {
                                    let kind = r.kind.to_uppercase();
                                    let is_w = r.kind == "worker";
                                    view! {
                                        <a class="me-chip" href="/map">
                                            <span class=if is_w { "me-chip-name worker" } else { "me-chip-name machine" }>{r.name}</span>
                                            <span class="me-chip-meta">{format!("{kind} ยท {}", r.role)}</span>
                                        </a>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ LAND โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"YOUR LAND"</span>
                        <a href="/map">"map โ†’"</a>
                    </div>
                    {move || {
                        let list = leases.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "No leases. "
                                    <a href="/map">"Lease a plot"</a>
                                    " on the Gesing map."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-chip-grid">
                                {list.into_iter().map(|l: Lease| {
                                    let href = format!("/map?plot={}", l.flat_id);
                                    let zone = if l.zone.is_empty() { "phase 0".into() } else { l.zone };
                                    view! {
                                        <a class="me-chip land" href=href>
                                            <span class="me-chip-name land">{l.flat_name.to_uppercase()}</span>
                                            <span class="me-chip-meta">{format!("{} ยท {}", l.flat_id, zone)}</span>
                                        </a>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ EVENTS โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"YOUR EVENTS"</span>
                        <a href="/calendar">"calendar โ†’"</a>
                    </div>
                    {move || {
                        let list = events.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "No events posted. "
                                    <a href="/calendar">"Add an event"</a>
                                    "."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-chip-grid">
                                {list.into_iter().map(|e: EventCard| {
                                    view! {
                                        <a class="me-chip event" href="/calendar">
                                            <span class="me-chip-name event">{e.title}</span>
                                            <span class="me-chip-meta">{format!("{} ยท {} ยท {}", e.city, e.when, e.status)}</span>
                                        </a>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ CITIES โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"CITIES YOU FOUNDED"</span>
                        <a href="/cities">"catalog โ†’"</a>
                    </div>
                    {move || {
                        let list = cities.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "No founding entries. "
                                    <a href="/cities">"Found a city"</a>
                                    "."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-chip-grid">
                                {list.into_iter().map(|c: CityCard| {
                                    let href = c.href.clone();
                                    view! {
                                        <a class="me-chip city" href=href>
                                            <span class="me-chip-name city">{c.name}</span>
                                            <span class="me-chip-meta">{format!("{} ยท {}", c.region, c.status)}</span>
                                        </a>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                // โ”€โ”€ INTENTS โ”€โ”€
                <section class="me-section">
                    <div class="me-section-h">
                        <span>"INTENT TAPE"</span>
                        <a href="/map">"console โ†’"</a>
                    </div>
                    {move || {
                        let list = intents.get();
                        if list.is_empty() {
                            return view! {
                                <div class="me-empty">
                                    "No intents yet โ€” buy, lease, split, merge, or assign on the "
                                    <a href="/map">"map"</a>
                                    "."
                                </div>
                            }.into_any();
                        }
                        view! {
                            <div class="me-intent-list">
                                {list.into_iter().take(24).map(|it: IntentRec| {
                                    let act = it.action.to_uppercase();
                                    view! {
                                        <div class="me-intent">
                                            <span class="ii-id">{format!("#{:03}", it.id)}</span>
                                            <span class="ii-fleet">{it.fleet}</span>
                                            <span class="ii-act">{act}</span>
                                            <span class="ii-flat">{it.flat.to_uppercase()}</span>
                                        </div>
                                    }
                                }).collect_view()}
                            </div>
                        }.into_any()
                    }}
                </section>

                <p class="bank-footnote">
                    "Soft3 local dashboard โ€” data stays in this browser (localStorage). No closed backend. Profile, CX, leases and intents sync with map ops."
                </p>
            </div>

            // edit profile sheet
            {move || edit_open.get().then(|| view! {
                <div class="cyberia-sheet-backdrop" on:click=move |_| edit_open.set(false)>
                    <div class="cyberia-sheet" on:click=move |ev| ev.stop_propagation()>
                        <div class="sheet-h">
                            <span class="panel-kicker">"EDIT PROFILE"</span>
                            <button class="sheet-x" on:click=move |_| edit_open.set(false)>"โœ•"</button>
                        </div>
                        <p class="sheet-note">"Handle and display name โ€” local only."</p>
                        <label class="found-label">"HANDLE"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="anon"
                            prop:value=move || draft_handle.get()
                            on:input=move |ev| draft_handle.set(event_target_value(&ev))
                        />
                        <label class="found-label">"DISPLAY NAME"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="your name"
                            prop:value=move || draft_name.get()
                            on:input=move |ev| draft_name.set(event_target_value(&ev))
                        />
                        <button
                            class="intent-commit"
                            on:click=move |_| {
                                let mut h = draft_handle.get().trim().to_lowercase();
                                h = h.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-').collect();
                                if h.is_empty() { h = "anon".into(); }
                                let name = {
                                    let n = draft_name.get().trim().to_string();
                                    if n.is_empty() { "Anonymous".into() } else { n }
                                };
                                let mut p = profile.get_untracked();
                                p.handle = h;
                                p.name = name;
                                save_profile(&p);
                                profile.set(p);
                                edit_open.set(false);
                            }
                        >
                            "SAVE"
                        </button>
                    </div>
                </div>
            })}

            <div class="search-dock cyberia-dock cities-dock">
                <span class="dock-count">
                    {move || {
                        let b: SoftBalance = balance.get();
                        format!(
                            "@{} ยท {} CX ยท {}r {}l {}e ยท activity {:.0}",
                            profile.get().handle,
                            fmt_cx(b.cx),
                            robots.get().len(),
                            leases.get().len(),
                            events.get().len(),
                            score(),
                        )
                    }}
                </span>
                <button class="cta-btn cta-lease cta-lg dock-found" on:click=move |_| {
                    // soft top-up for demo
                    let mut b = balance.get_untracked();
                    b.cx += 25.0;
                    save_balance(&b);
                    balance.set(b);
                }>
                    <span class="cta-copy">
                        <span class="cta-title">"+25 CX"</span>
                        <span class="cta-sub">"mock top-up"</span>
                    </span>
                </button>
                <a href="https://x.com/cyberiacap" target="_blank" rel="noopener" class="dock-credit">
                    "๐Ÿด a "<span style="color: var(--cyber-green);">"cyberia"</span>" project"
                </a>
            </div>
        </div>
    }
}

Homonyms

cyberia/cyberia-my/src/me.rs

Graph