akefin-design-system/ui_kits/web/ReviewQueue.jsx
2026-05-23 11:59:45 +09:00

125 lines
5.3 KiB
JavaScript

// 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 });