import { Button } from '@/components/ui/button' import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import _ from '@/lib/translate' import { useAtomValue, useSetAtom } from 'jotai' import { ArrowDownRight, ArrowRightLeftIcon, ArrowUpRight, CalendarIcon, CircleXIcon, GitCompareIcon, HistoryIcon, LandmarkIcon, Loader2Icon, ReceiptIcon, ReceiptTextIcon, UserIcon, WalletIcon } from 'lucide-react' import { useMemo, useState } from 'react' import { ActionLogItem, ActionLog as ActionLogType, bankRecActionLog, bankRecDateAtom, bankRecMatchFilters, SelectedBank, selectedBankAccountAtom } from '../BankReconciliation/bankRecAtoms' import { useHotkeys } from 'react-hotkeys-hook' import { useGetBankAccounts } from '../BankReconciliation/utils' import { getCompanyCurrency } from '@/lib/company' import { formatCurrency } from '@/lib/numbers' import dayjs from 'dayjs' import { cn } from '@/lib/utils' import { formatDate } from '@/lib/date' import { Separator } from '@/components/ui/separator' import { slug } from '@/lib/frappe' import { PaymentEntry } from '@/types/Accounts/PaymentEntry' import { JournalEntry } from '@/types/Accounts/JournalEntry' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { Table, TableCell, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog' import { useFrappePostCall, useSWRConfig } from 'frappe-react-sdk' import { toast } from 'sonner' import { getErrorMessage } from '@/lib/frappe' import ErrorBanner from '@/components/ui/error-banner' import SelectedTransactionDetails from '../BankReconciliation/SelectedTransactionDetails' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty' import BankLogo from '@/components/common/BankLogo' const ActionLog = () => { const [isOpen, setIsOpen] = useState(false) useHotkeys('meta+z', () => { setIsOpen(true) }, { enabled: true, enableOnFormTags: false, preventDefault: true }) return ( {_("Reconciliation History")} {_("Reconciliation History")} {_("View all reconciliation actions taken in this session.")} ) } const ActionLogDialogContent = () => { const actionLog = useAtomValue(bankRecActionLog) return
{actionLog.map((action) => (
{action.items.map((item, index) => ( ))}
))} {actionLog.length === 0 && {_("No reconciliation actions found")} {_("You have not performed any reconciliations in this session yet.")} }
} const ActionGroupHeader = ({ action }: { action: ActionLogType }) => { const label = useMemo(() => { switch (action.type) { case 'match': return _("Matched") case 'payment': if (action.isBulk) { return _("Bulk Payment") } return _("Payment") case 'transfer': if (action.isBulk) { return _("Bulk Transfer") } return _("Transfer") case 'bank_entry': if (action.isBulk) { return _("Bulk Bank Entry") } return _("Bank Entry") default: return _("Action") } }, [action]) return
{action.type === 'match' && } {action.type === 'payment' && } {action.type === 'transfer' && } {action.type === 'bank_entry' && } {label} - {dayjs(action.timestamp).fromNow()}
} const Row = ({ item, index, isLast, action }: { item: ActionLogItem, index: number, isLast: boolean, action: ActionLogType }) => { const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 const { banks } = useGetBankAccounts() const bank = useMemo(() => { if (item.bankTransaction.bank_account) { return banks?.find((bank) => bank.name === item.bankTransaction.bank_account) } return null }, [item.bankTransaction.bank_account, banks]) const amount = item.bankTransaction.withdrawal ? item.bankTransaction.withdrawal : item.bankTransaction.deposit const currency = item.bankTransaction.currency || getCompanyCurrency(item.bankTransaction.company ?? '') return

{item.bankTransaction.description}

{item.bankTransaction.bank_account}
{formatDate(item.bankTransaction.date, 'Do MMM YYYY')}
{isWithdrawal ? : } {formatCurrency(amount, currency)}
{["Payment Entry", "Journal Entry"].includes(item.voucher.reference_doctype) ? "" : _("{} :", [item.voucher.reference_doctype])} {item.voucher.reference_name} {item.voucher.reference_doctype === "Payment Entry" && item.voucher.doc && } {item.voucher.reference_doctype === "Journal Entry" && }
} const JournalEntryDetails = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => { return
} const JournalEntryAccountsTable = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => { const accounts = useMemo(() => { const allAccounts = (item.voucher.doc as JournalEntry).accounts return allAccounts.filter((acc) => bank ? acc.account !== bank.account : true) }, [item, bank]) return <> {accounts.length === 1 ? {accounts[0].account} : {_("Split across {} accounts", [accounts.length.toString()])} {_("Account")} {_("Debit")} {_("Credit")} {accounts.map((account) => ( {account.account} {formatCurrency(account.debit ?? 0, account.account_currency ?? '')} {formatCurrency(account.credit ?? 0, account.account_currency ?? '')} ))}
} } const PaymentEntryDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => { if ((item.voucher.doc as PaymentEntry).payment_type === "Internal Transfer") { return } const invoices = (item.voucher.doc as PaymentEntry).references ?? [] const currency = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 ? (item.voucher.doc as PaymentEntry)?.paid_to_account_currency : (item.voucher.doc as PaymentEntry)?.paid_from_account_currency return
{(item.voucher.doc as PaymentEntry).party_name}
{invoices.length === 0 ? _("No invoice linked") : invoices.length === 1 ? _("1 invoice") : _("{} invoices", [invoices.length.toString()])}
{invoices.map((invoice) => ( {_("Document")} {_("Invoice No")} {_("Due Date")} {_("Grand Total")} {_("Allocated")} {invoice.reference_doctype}: {invoice.reference_name} {invoice.bill_no ?? "-"} {formatDate(invoice.due_date)} {formatCurrency(invoice.total_amount, currency ?? '')} {formatCurrency(invoice.allocated_amount, currency ?? '')}
))}
} const TransferDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => { const { banks } = useGetBankAccounts() const bank = useMemo(() => { const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 let transferAccount = "" if (isWithdrawal) { transferAccount = (item.voucher.doc as PaymentEntry).paid_to } else { transferAccount = (item.voucher.doc as PaymentEntry).paid_from } const transferBankAccount = banks?.find((bank) => bank.account === transferAccount) return transferBankAccount }, [banks, item]) return
{bank?.account}
} const ACTION_TYPE_MAP = { 'bank_entry': _("Bank Entry"), 'payment': _("Payment"), 'transfer': _("Transfer"), 'match': _("Match"), } const CancelActionLogItem = ({ item, type, timestamp, bank }: { item: ActionLogItem, type: ActionLogType['type'], timestamp: number, bank?: SelectedBank | null }) => { const [isOpen, setIsOpen] = useState(false) const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unreconcile_transaction_entry') const { mutate } = useSWRConfig() const actionLog = useSetAtom(bankRecActionLog) const dates = useAtomValue(bankRecDateAtom) const matchFilters = useAtomValue(bankRecMatchFilters) const selectedBank = useAtomValue(selectedBankAccountAtom) const onUndo = () => { call({ bank_transaction_id: item.bankTransaction.name, voucher_type: item.voucher.reference_doctype, voucher_id: item.voucher.reference_name, }).then(() => { toast.success(type === 'match' ? _("Unmatched") : _("Cancelled")) if (selectedBank?.name === item.bankTransaction.bank_account) { mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`) mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`) // Update the matching vouchers for the selected transaction mutate(`bank-reconciliation-vouchers-${item.bankTransaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`) } setTimeout(() => { actionLog((prev) => { // Find the action and then remove the item from the action. If the action is empty, remove the action from the array const action = prev.find((action) => action.timestamp === timestamp) if (action) { action.items = action.items.filter((i) => i.bankTransaction.name !== item.bankTransaction.name) } // If the action is empty, remove the action from the array if (action && action.items.length === 0) { return prev.filter((a) => a.timestamp !== timestamp) } else { return prev.map((a) => a.timestamp === timestamp ? { ...a, items: action?.items ?? [] } : a) } }) }, 100) setIsOpen(false) }).catch((error) => { toast.error(_("There was an error while performing the action."), { duration: 5000, description: getErrorMessage(error), }) }) } return {_("Cancel")} {type === 'match' ? _("Unmatch Transaction?") : _("Undo {}?", [item.voucher.reference_doctype])} {type === 'match' ? _("Are you sure you want to unmatch the voucher from this transaction?") : _("Are you sure you want to cancel this {} {}?", [_(item.voucher.reference_doctype), item.voucher.reference_name])} {error && }
{_("Action Type")} {ACTION_TYPE_MAP[type]} {_("Voucher Type")} {_(item.voucher.reference_doctype)} {_("Voucher Name")} {item.voucher.reference_name} {_("Posting Date")} {formatDate(item.voucher.posting_date, 'Do MMM YYYY')} {type === 'transfer' && item.voucher.doc && {_("Transfer Account")} } {type === 'payment' && item.voucher.doc && {_("Payment Details")} } {type === 'bank_entry' && item.voucher.doc && {_("Account")} }
{_("Close")}
} export default ActionLog