first commit
This commit is contained in:
commit
8b790b7601
86 changed files with 6348 additions and 0 deletions
125
ui_kits/web/ReviewQueue.jsx
Normal file
125
ui_kits/web/ReviewQueue.jsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// ReviewQueue — compact list of staged transactions
|
||||
|
||||
function ReviewQueue({ entity, transactions, selectedId, onSelect, sort, setSort, filters, setFilters, onOpenPalette }) {
|
||||
const [filterOpen, setFilterOpen] = React.useState(false);
|
||||
|
||||
// Apply filters
|
||||
const cutoffDays = { today: 0, "7d": 7, "30d": 30, "this-month": 30 };
|
||||
const filtered = transactions.filter(t => {
|
||||
if (entity !== "all" && t.entity !== entity) return false;
|
||||
// Date
|
||||
if (filters.dateRange !== "all" && filters.dateRange !== "custom") {
|
||||
const days = cutoffDays[filters.dateRange];
|
||||
const cutoff = new Date("2026-03-08"); // anchor "now"
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
if (new Date(t.date) < cutoff) return false;
|
||||
} else if (filters.dateRange === "custom") {
|
||||
if (filters.dateFrom && t.date < filters.dateFrom) return false;
|
||||
if (filters.dateTo && t.date > filters.dateTo) return false;
|
||||
}
|
||||
// Categories
|
||||
if (filters.categories.size > 0) {
|
||||
const cat = (t.suggestedAccount || "").split(":").slice(2).join(":");
|
||||
const hit = [...filters.categories].some(c => cat.includes(c));
|
||||
if (!hit) return false;
|
||||
}
|
||||
// Tiers
|
||||
if (filters.tiers.size > 0 && !filters.tiers.has(t.tier)) return false;
|
||||
// Confidence
|
||||
if ((t.score ?? 0) < filters.confidenceMin) return false;
|
||||
// Currency
|
||||
if (filters.ccy.size > 0 && !filters.ccy.has(t.ccy)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const activeCount =
|
||||
(filters.dateRange !== "all" ? 1 : 0) +
|
||||
(filters.categories.size > 0 ? 1 : 0) +
|
||||
(filters.tiers.size > 0 ? 1 : 0) +
|
||||
(filters.ccy.size > 0 ? 1 : 0) +
|
||||
(filters.confidenceMin > 0 ? 1 : 0);
|
||||
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
if (sort === "date-desc") return b.date.localeCompare(a.date);
|
||||
if (sort === "date-asc") return a.date.localeCompare(b.date);
|
||||
if (sort === "conf-asc") return (a.score ?? -1) - (b.score ?? -1);
|
||||
if (sort === "conf-desc") return (b.score ?? -1) - (a.score ?? -1);
|
||||
if (sort === "entity") return a.entity.localeCompare(b.entity);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="review-pane">
|
||||
<div className="rq-head">
|
||||
<div className="rq-tabs">
|
||||
<button className="rq-tab active">Staged <span className="rq-count">{filtered.length}</span></button>
|
||||
<button className="rq-tab">Approved <span className="rq-count">312</span></button>
|
||||
<button className="rq-tab">Skipped <span className="rq-count">4</span></button>
|
||||
</div>
|
||||
<div className="rq-controls">
|
||||
<button className={`rq-ctrl ${activeCount > 0 ? "active" : ""}`} onClick={() => setFilterOpen(o => !o)}>
|
||||
<Icon name="filter" size={13} /> FILTER
|
||||
{activeCount > 0 && <span className="rq-ctrl-dot">{activeCount}</span>}
|
||||
</button>
|
||||
<button className="rq-ctrl" onClick={onOpenPalette}>
|
||||
<Icon name="spark" size={13} /> ⌘K
|
||||
</button>
|
||||
<div className="rq-sort">
|
||||
<Icon name="sort" size={13} />
|
||||
<select value={sort} onChange={e => setSort(e.target.value)}>
|
||||
<option value="date-desc">Date · newest</option>
|
||||
<option value="date-asc">Date · oldest</option>
|
||||
<option value="conf-asc">Confidence · lowest</option>
|
||||
<option value="conf-desc">Confidence · highest</option>
|
||||
<option value="entity">Entity</option>
|
||||
</select>
|
||||
<Icon name="chevDown" size={12} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilterPanel open={filterOpen} filters={filters} setFilters={setFilters}
|
||||
onClose={() => setFilterOpen(false)} activeCount={activeCount} />
|
||||
|
||||
<div className="rq-list-head">
|
||||
<span>DATE</span>
|
||||
<span>PAYEE</span>
|
||||
<span style={{textAlign:"right"}}>AMOUNT</span>
|
||||
<span>ENTITY</span>
|
||||
<span>CONFIDENCE</span>
|
||||
<span>TIER</span>
|
||||
</div>
|
||||
|
||||
<div className="rq-list">
|
||||
{sorted.map(t => (
|
||||
<button key={t.id}
|
||||
className={`tx-row ${selectedId === t.id ? "selected" : ""}`}
|
||||
onClick={() => onSelect(t.id)}>
|
||||
<span className="tx-date">{t.date.slice(5)}</span>
|
||||
<span className="tx-payee">
|
||||
<span className="ko">{t.payee}</span>
|
||||
{t.payeeNote && <span className="tx-payee-note">{t.payeeNote}</span>}
|
||||
</span>
|
||||
<span className="tx-amount-cell"><Amount value={t.amount} ccy={t.ccy} /></span>
|
||||
<span><EntityBadge entity={t.entity} /></span>
|
||||
<span><ConfidenceChip score={t.score} tier={t.tier} /></span>
|
||||
<span><TierBadge tier={t.tier} /></span>
|
||||
</button>
|
||||
))}
|
||||
{sorted.length === 0 && (
|
||||
<div className="rq-empty">
|
||||
<div className="rq-empty-title">No transactions match</div>
|
||||
<div className="rq-empty-sub">Clear filters or expand the date range</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rq-foot">
|
||||
<span className="rq-foot-meta">{sorted.length} of {transactions.filter(t => entity === "all" || t.entity === entity).length} · sorted by {sort.replace("-", " ")}{activeCount > 0 ? ` · ${activeCount} filter${activeCount > 1 ? "s" : ""}` : ""}</span>
|
||||
<span className="rq-foot-meta">⏎ approve · ⌫ skip · ⌘E override · ⌘K commands</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { ReviewQueue });
|
||||
Loading…
Add table
Add a link
Reference in a new issue