124 lines
5.1 KiB
JavaScript
124 lines
5.1 KiB
JavaScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { ChevronDown, Trash2, ToggleLeft, ToggleRight, Settings2 } from 'lucide-react';
|
|
import { api } from '@/api';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
export default function BillRulesManager() {
|
|
const [open, setOpen] = useState(false);
|
|
const [rules, setRules] = useState([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const d = await api.allBillMerchantRules();
|
|
setRules(d.rules || []);
|
|
} catch (err) {
|
|
toast.error(err.message || 'Failed to load bill matching rules');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { if (open) load(); }, [open, load]);
|
|
|
|
const handleDelete = async (billId, ruleId, merchant) => {
|
|
try {
|
|
await api.deleteMerchantRule(billId, ruleId);
|
|
setRules(prev => prev.filter(r => r.id !== ruleId));
|
|
toast.success(`Rule "${merchant}" removed`);
|
|
} catch (err) {
|
|
toast.error(err.message || 'Failed to delete rule');
|
|
}
|
|
};
|
|
|
|
const handleToggleAutoLate = async (billId, ruleId, current) => {
|
|
try {
|
|
await api.toggleRuleAutoAttribute(billId, ruleId, !current);
|
|
setRules(prev => prev.map(r =>
|
|
r.id === ruleId ? { ...r, auto_attribute_late: current ? 0 : 1 } : r
|
|
));
|
|
} catch (err) {
|
|
toast.error(err.message || 'Failed to update rule');
|
|
}
|
|
};
|
|
|
|
// Group rules by bill
|
|
const byBill = rules.reduce((acc, r) => {
|
|
const key = r.bill_id;
|
|
if (!acc[key]) acc[key] = { bill_name: r.bill_name, bill_id: r.bill_id, rules: [] };
|
|
acc[key].rules.push(r);
|
|
return acc;
|
|
}, {});
|
|
const groups = Object.values(byBill);
|
|
|
|
return (
|
|
<div className="rounded-xl border border-border/60 bg-card/80 overflow-hidden">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(v => !v)}
|
|
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/20 transition-colors"
|
|
>
|
|
<Settings2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium flex-1">Bill Matching Rules</span>
|
|
{!open && rules.length > 0 && (
|
|
<Badge variant="secondary" className="text-xs">{rules.length}</Badge>
|
|
)}
|
|
<ChevronDown className={`h-4 w-4 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="border-t border-border/40">
|
|
{loading ? (
|
|
<p className="text-sm text-muted-foreground text-center py-6">Loading…</p>
|
|
) : groups.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-6 px-4">
|
|
No rules saved yet. Open a bill and add merchant matching rules to auto-match bank transactions.
|
|
</p>
|
|
) : (
|
|
<div className="divide-y divide-border/30 max-h-96 overflow-y-auto">
|
|
{groups.map(group => (
|
|
<div key={group.bill_id}>
|
|
<div className="px-4 py-2 bg-muted/20">
|
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide truncate">
|
|
{group.bill_name}
|
|
</p>
|
|
</div>
|
|
{group.rules.map(rule => (
|
|
<div key={rule.id} className="flex items-center gap-3 px-4 py-2.5 hover:bg-muted/10 transition-colors">
|
|
<span className="font-mono text-xs flex-1 truncate">{rule.merchant}</span>
|
|
<button
|
|
type="button"
|
|
title={rule.auto_attribute_late ? 'Auto-apply late attribution — click to disable' : 'Enable auto late attribution'}
|
|
onClick={() => handleToggleAutoLate(group.bill_id, rule.id, !!rule.auto_attribute_late)}
|
|
className={`shrink-0 transition-colors ${rule.auto_attribute_late ? 'text-primary' : 'text-muted-foreground/40 hover:text-muted-foreground'}`}
|
|
>
|
|
{rule.auto_attribute_late
|
|
? <ToggleRight className="h-4 w-4" />
|
|
: <ToggleLeft className="h-4 w-4" />
|
|
}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDelete(group.bill_id, rule.id, rule.merchant)}
|
|
className="shrink-0 text-muted-foreground hover:text-destructive transition-colors"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
<p className="px-4 py-2 text-[11px] text-muted-foreground border-t border-border/30">
|
|
Merchant patterns are matched with word-boundary rules. Toggle icon = auto-apply late month attribution.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|