// Mobile screens — Queue, Detail, Picker (sheet), Import, Ledger
// ──────────────────────────────────────────────────────────────────
// Header — universal mobile top bar
// ──────────────────────────────────────────────────────────────────
function MHeader({ left, title, right, eyebrow }) {
return (
{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,
});