import { Button } from "@/components/ui/button" import ErrorBanner from "@/components/ui/error-banner" import { Skeleton } from "@/components/ui/skeleton" import { Badge } from "@/components/ui/badge" import _ from "@/lib/translate" import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappeGetDocList, useFrappePostCall } from "frappe-react-sdk" import { ArrowDownRight, ArrowDownUp, ArrowUpRight, MoreVertical, Trash2, GripVertical, Play, RefreshCw, ZapIcon, CalendarSyncIcon } from "lucide-react" import { useContext, useState } from "react" import { toast } from "sonner" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu" import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" import { cn } from "@/lib/utils" const useGetRuleList = () => { return useFrappeGetDocList("Bank Transaction Rule", { fields: ["name", "rule_name", "rule_description", "transaction_type", "priority"], orderBy: { field: 'priority', order: 'asc' }, limit: 100 }) } export const RunRulesButton = () => { const { data } = useGetRuleList() const { call: runRuleEvaluation, loading: isRunningRules } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction_rule.bank_transaction_rule.run_rule_evaluation') const handleRunRules = async (forceEvaluate: boolean = false) => { try { await runRuleEvaluation({ force_evaluate: forceEvaluate }) toast.success(forceEvaluate ? _("Rules evaluation started") : _("Rules evaluation completed")) } catch (error) { toast.error(_("Failed to run rules evaluation")) console.error("Error running rules evaluation:", error) } } if (!data || data.length === 0) { return null } return handleRunRules(false)} disabled={isRunningRules} title={_("Run rules on unreconciled transactions that haven't been evaluated yet")}> {_("Run on new transactions")} handleRunRules(true)} disabled={isRunningRules} title={_("Force re-evaluate all unreconciled transactions, even if they were previously evaluated")}> {_("Force evaluate all")} } const AutoRunRuleItem = () => { const { db } = useContext(FrappeContext) as FrappeConfig const { data: accountsSetting, mutate: setAutomaticallyRunRulesOnUnreconciledTransactions } = useFrappeGetCall("frappe.client.get_single_value", { "doctype": "Accounts Settings", "field": "automatically_run_rules_on_unreconciled_transactions" }) const automaticallyRunRulesOnUnreconciledTransactions = accountsSetting?.message ? true : false const onAutoClassifyTransactions = (checked: boolean) => { toast.promise(db.setValue("Accounts Settings", "Accounts Settings", "automatically_run_rules_on_unreconciled_transactions", checked ? 1 : 0).then(() => { setAutomaticallyRunRulesOnUnreconciledTransactions({ message: { automatically_run_rules_on_unreconciled_transactions: checked ? 1 : 0, } }, { revalidate: false }) }), { loading: _("Updating..."), success: checked ? _("Scheduled job enabled. Transactions will be auto classified.") : _("Scheduled job disabled. Transactions will not be auto classified."), error: _("Failed to update auto classify transactions settings") }) } return {_("Run rules automatically")} } const RuleList = ({ setSelectedRule }: { setSelectedRule: (rule: string) => void }) => { const { data, error, isLoading, mutate } = useGetRuleList() const { db } = useContext(FrappeContext) as FrappeConfig const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ) const onDeleteRule = (ruleID: string) => { toast.promise(db.deleteDoc("Bank Transaction Rule", ruleID).then(() => { mutate() }), { loading: _("Deleting rule..."), success: _("Rule deleted."), error: _("Failed to delete rule.") }) } const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event if (active.id !== over?.id && data) { const oldIndex = data.findIndex((rule) => rule.name === active.id) const newIndex = data.findIndex((rule) => rule.name === over?.id) const newData = arrayMove(data, oldIndex, newIndex) // Update priorities based on new order const updatePromises = newData.map((rule, index) => { const newPriority = index + 1 if (rule.priority !== newPriority) { return db.setValue("Bank Transaction Rule", rule.name, "priority", newPriority) } return Promise.resolve() }) try { await Promise.all(updatePromises) toast.success(_("Rule priorities updated")) mutate() // Refresh the data } catch (error) { toast.error(_("Failed to update rule priorities")) console.error("Error updating priorities:", error) } } } return ( <>
{isLoading &&
} {error && } {data && data.length === 0 && {_("No rules setup yet")} {_("Configure rules to save time when reconciling transactions.")} } {data && data.length > 0 && ( rule.name)} strategy={verticalListSortingStrategy} >
    {data?.map((rule) => ( ))}
)}
) } const SortableRuleItem = ({ rule, setSelectedRule, onDeleteRule }: { rule: BankTransactionRule setSelectedRule: (rule: string) => void onDeleteRule: (ruleID: string) => void }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: rule.name }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } const [isDropdownOpen, setIsDropdownOpen] = useState(false) return (
  • {rule.priority}
    {rule.transaction_type === "Any" ? : rule.transaction_type === "Withdrawal" ? : }
    {rule.rule_description}
    onDeleteRule(rule.name)}> {_("Delete")}
  • ) } export default RuleList