// Mobile screens — Queue, Detail, Picker (sheet), Import, Ledger // ────────────────────────────────────────────────────────────────── // Header — universal mobile top bar // ────────────────────────────────────────────────────────────────── function MHeader({ left, title, right, eyebrow }) { return (
{left}
{right}
{eyebrow &&
{eyebrow}
} {title &&
{title}
}
); } // Tab bar function MTabBar({ active, onChange }) { const tabs = [ { id: "queue", icon: "activity", label: "Review" }, { id: "rules", icon: "rule", label: "Rules" }, { id: "ledger", icon: "ledger", label: "Ledger" }, { id: "import", icon: "import", label: "Import" }, ]; return (
{tabs.map(t => ( ))}
); } // ────────────────────────────────────────────────────────────────── // 1. QUEUE SCREEN — list of staged transactions // ────────────────────────────────────────────────────────────────── function MQueueScreen({ transactions, entity, onTap }) { const filtered = transactions.filter(t => entity === "all" || t.entity === entity); return (
} right={} />
SCOPE {entity === "all" ? "All entities" : entity === "personal" ? "Personal" : entity === "tfox" ? "9TFox" : "Finacode"} {filtered.length} STAGED
{filtered.map(t => ( ))}
); } // ────────────────────────────────────────────────────────────────── // 2. DETAIL SCREEN — full review for one transaction // ────────────────────────────────────────────────────────────────── function MDetailScreen({ tx, onBack, onApprove, onSkip, onOpenPicker, account }) { if (!tx) return null; const sourceAccount = tx.entity === "personal" ? "Personal:Assets:Bank:Toss" : tx.entity === "tfox" ? "9TFox:Assets:Bank:Kakao" : "Finacode:Assets:Bank:Wise"; const isExpense = tx.amount < 0; const debit = isExpense ? account : sourceAccount; const credit = isExpense ? sourceAccount : account; const abs = Math.abs(tx.amount).toLocaleString(); return (
} right={} />
{tx.payee}
{tx.payeeNote &&
{tx.payeeNote}
}
{tx.date} · · {tx.sourceAccount}
AI SUGGESTION
{tx.suggestedAccount || No suggestion · unmatched}
matched by {tx.tier === "rules" ? "pattern rule" : tx.tier === "llm" ? "LLM (gpt-4o-mini)" : tx.tier === "agent" ? "agent · 3 tool calls" : "no tier — needs override"}
CATEGORIZE TO
LEDGER PREVIEW
{account ? (
{debit} {abs} {tx.ccy}
{credit} -{abs} {tx.ccy}
balanced · 2 legs
) : (
Choose an account to preview the posting
)}
); } // ────────────────────────────────────────────────────────────────── // 3. ACCOUNT PICKER — modal sheet // ────────────────────────────────────────────────────────────────── function MAccountPicker({ open, onClose, onPick, current }) { const [q, setQ] = React.useState(""); if (!open) return null; const accounts = window.AKEFIN_DATA.allAccounts; const mru = ["Personal:Expenses:Food:Coffee", "Personal:Expenses:Groceries", "9TFox:Expenses:Software"]; const matches = q ? accounts.filter(a => a.toLowerCase().includes(q.toLowerCase())) : accounts; return (
e.stopPropagation()}>
Pick account
setQ(e.target.value)} />
{!q && (
RECENT
{mru.map(a => ( ))}
)}
{q ? "MATCHES" : "ALL ACCOUNTS"}
{matches.map(a => ( ))}
); } // ────────────────────────────────────────────────────────────────── // 4. IMPORT SCREEN // ────────────────────────────────────────────────────────────────── function MImportScreen({ entity }) { const runs = window.AKEFIN_DATA.importRuns.filter(r => entity === "all" || r.entity === entity); const total = runs.reduce((acc, r) => ({ rows: acc.rows + r.rows, auto: acc.auto + r.auto, high: acc.high + r.high, review: acc.review + r.review, failed: acc.failed + r.failed, }), { rows: 0, auto: 0, high: 0, review: 0, failed: 0 }); return (
} />
{total.rows}TOTAL
{total.auto}AUTO
{total.review}REVIEW
{total.failed}FAILED
RECENT RUNS
{runs.map(r => (
{r.at}
{r.source}
{r.rows} rows · {r.auto} auto · {r.review} review {r.failed > 0 && <>·{r.failed} failed}
))}
); } // ────────────────────────────────────────────────────────────────── // 5. LEDGER SCREEN // ────────────────────────────────────────────────────────────────── function MLedgerScreen({ entity }) { const data = window.AKEFIN_DATA.accountsByEntity; const entities = entity === "all" ? ["personal", "tfox", "finacode"] : [entity]; return (
{entities.map(e => { const map = window.AKEFIN_DATA.entities.find(x => x.id === e); const accounts = data[e] || []; return (
{map.label} {accounts.length} accts
{accounts.map(a => (
{a.path.split(":").slice(-1)[0]} {a.path}
))}
); })}
); } Object.assign(window, { MHeader, MTabBar, MQueueScreen, MDetailScreen, MAccountPicker, MImportScreen, MLedgerScreen, });