72 lines
3 KiB
JavaScript
72 lines
3 KiB
JavaScript
// MobileApp — stateful navigation between screens for ONE device frame
|
|
|
|
function MobileApp({ initial = "queue", initialTx = null, initialEntity = "all", initialPickerOpen = false }) {
|
|
const [screen, setScreen] = React.useState(initial);
|
|
const [entity, setEntity] = React.useState(initialEntity);
|
|
const [transactions, setTransactions] = React.useState(window.AKEFIN_DATA.transactions);
|
|
const [activeTxId, setActiveTxId] = React.useState(initialTx);
|
|
const [pickerOpen, setPickerOpen] = React.useState(initialPickerOpen);
|
|
const tx = activeTxId ? transactions.find(t => t.id === activeTxId) : null;
|
|
const [account, setAccount] = React.useState(tx?.suggestedAccount || null);
|
|
|
|
React.useEffect(() => {
|
|
if (tx) setAccount(tx.suggestedAccount || null);
|
|
}, [tx?.id]);
|
|
|
|
const openTx = (id) => { setActiveTxId(id); setScreen("detail"); };
|
|
const back = () => { setScreen("queue"); setActiveTxId(null); };
|
|
const approve = () => {
|
|
setTransactions(prev => prev.filter(t => t.id !== activeTxId));
|
|
back();
|
|
};
|
|
const skip = () => {
|
|
setTransactions(prev => prev.filter(t => t.id !== activeTxId));
|
|
back();
|
|
};
|
|
|
|
return (
|
|
<div className="m-app">
|
|
<div className="m-app-body">
|
|
{screen === "queue" && <MQueueScreen transactions={transactions} entity={entity} onTap={openTx} />}
|
|
{screen === "detail" && <MDetailScreen tx={tx} onBack={back} onApprove={approve} onSkip={skip} account={account} onOpenPicker={() => setPickerOpen(true)} />}
|
|
{screen === "rules" && <MRulesScreen entity={entity} />}
|
|
{screen === "ledger" && <MLedgerScreen entity={entity} />}
|
|
{screen === "import" && <MImportScreen entity={entity} />}
|
|
</div>
|
|
<MTabBar active={screen === "detail" ? "queue" : screen} onChange={(s) => { setActiveTxId(null); setScreen(s); }} />
|
|
<MAccountPicker open={pickerOpen} onClose={() => setPickerOpen(false)}
|
|
onPick={(a) => { setAccount(a); setPickerOpen(false); }}
|
|
current={account} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// A tiny rules screen for mobile
|
|
function MRulesScreen({ entity }) {
|
|
const rules = window.AKEFIN_DATA.ruleSuggestions;
|
|
return (
|
|
<div className="m-screen">
|
|
<MHeader eyebrow="AI SUGGESTIONS" title="Rules" />
|
|
<div className="m-list">
|
|
{rules.map(r => (
|
|
<div key={r.id} className="m-rule-card">
|
|
<div className="m-rule-top">
|
|
<code className="m-rule-pattern ko">"{r.pattern}"</code>
|
|
<MConfidenceChip score={r.score} tier="llm" />
|
|
</div>
|
|
<div className="m-rule-map">
|
|
<span className="m-arrow">→</span>
|
|
<span className="mono">{r.target}</span>
|
|
</div>
|
|
<div className="m-rule-foot">
|
|
<span className="m-rule-occ"><span className="num">{r.occurrences}</span> matches · 30d</span>
|
|
<button className="m-rule-promote">Promote ✓</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, { MobileApp, MRulesScreen });
|