import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogTitle, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog" import { FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form" import { AccountFormField, CurrencyFormField, DataField, LinkFormField, PartyTypeFormField, SelectFormField, SmallTextField } from "@/components/ui/form-elements" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { SelectItem } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { H4, Paragraph } from "@/components/ui/typography" import { today } from "@/lib/date" import { evaluateAmountFormula } from "@/lib/amountFormula" import _ from "@/lib/translate" import { cn } from "@/lib/utils" import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule" import { BankTransactionRuleAccounts } from "@/types/Accounts/BankTransactionRuleAccounts" import { FrappeConfig, FrappeContext } from "frappe-react-sdk" import { ArrowDownRight, ArrowDownUp, ArrowRightLeftIcon, ArrowUpRight, LandmarkIcon, Plus, PlusCircleIcon, ReceiptIcon, Settings, Trash2 } from "lucide-react" import { ChangeEvent, useCallback, useContext, useMemo, useRef, useState } from "react" import { useFieldArray, useFormContext, useWatch } from "react-hook-form" export const RuleForm = ({ isEdit = false }: { isEdit?: boolean }) => { return
} const CompanySelector = () => { const { setValue } = useFormContext() return { setValue('account', '') } }} /> } /** Component to render a radio group as a toggle group with options for All, Withdrawal, Deposit */ const TransactionTypeSelector = () => { const { control } = useFormContext() return ( ( {_("Transaction Type")}* {_("All")} {_("Withdrawal")} {_("Deposit")} )} /> ) } const DescriptionRules = () => { const { control } = useFormContext() const { fields, append, remove } = useFieldArray({ control, name: "description_rules" }) const addRow = () => { // @ts-expect-error - we don't need all fields here append({ check: "Contains" }) } return (
{_("Rules to match against the transaction description")} * {fields.map((field, index) => (
{_("Contains")} {_("Starts with")} {_("Ends with")} {_("Regex")}
))}
) } const RuleAction = () => { const { control } = useFormContext() const classify_as = useWatch({ control, name: "classify_as" }) const party_type = useWatch({ control, name: "party_type" }) const bank_entry_type = useWatch({ control, name: "bank_entry_type" }) const accountType = useMemo(() => { if (classify_as === "Payment Entry") { return party_type === "Supplier" ? ["Payable"] : ["Receivable"] } if (classify_as === "Transfer") { return ["Bank", "Cash", "Temporary"] } return undefined }, [classify_as, party_type]) return (

{_("If rule matches, then:")}

{_("Bank Entry")} {_("Payment Entry")} {_("Transfer")} {classify_as === "Bank Entry" && ( {_("Single Account")} {_("Multiple Accounts (Journal Template)")} )} {classify_as === "Payment Entry" && (
)} {(((bank_entry_type === "Single Account" || !bank_entry_type) && classify_as === "Bank Entry") || classify_as !== "Bank Entry") && ()} {bank_entry_type === "Multiple Accounts" && classify_as === "Bank Entry" && }
) } const PartyField = () => { const { control, setValue } = useFormContext() const party_type = useWatch({ control, name: `party_type` }) const { call } = useContext(FrappeContext) as FrappeConfig const company = useWatch({ control, name: 'company' }) const onChange = (event: ChangeEvent) => { // Fetch the party and account if (event.target.value) { call.get('erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details', { company: company, party_type: party_type, party: event.target.value, date: today() }).then((res) => { setValue('account', res.message.party_account) }) } else { // Clear the account setValue('account', '') } } if (!party_type) { return } return } const MultipleAccountsSelection = () => { const { control } = useFormContext() const accounts = useWatch({ control, name: 'accounts' }) ?? [] const [isConfigureAccountsModalOpen, setIsConfigureAccountsModalOpen] = useState(false) return
{_("Account")} {_("Debit")} {_("Credit")} {accounts.length === 0 && (
{_("No accounts configured")}
)} {accounts.map((account, index) => ( {account.account} {index === accounts.length - 1 ? {_("This is auto computed to balance the journal entry.")} {_("Based on the above entries, the balance amount (debit or credit) will be set for the last row to balance the journal entry.")} : <> } ))}
setIsConfigureAccountsModalOpen(false)} />
} const AmountFormulaRenderer = ({ value }: { value?: string }) => { // If it's a string and cannot be a number, then show it as a formula if (isNaN(Number(value))) { let calculatedValue = ""; try { calculatedValue = String(evaluateAmountFormula(value ?? "", 200)); } catch (error: unknown) { console.error(error); calculatedValue = "Error"; } const isComputationValid = !isNaN(Number(calculatedValue)) && calculatedValue !== undefined && calculatedValue !== null; return {value}

{isComputationValid ? _("This is a formula based value.") : _("This is not a valid formula. Check the variable used in the formula.")}

{_("Example: If the transaction amount is 200, then this will be calculated as {} = {}", [value ?? "", calculatedValue])}

} return {value} } const ConfigureAccountsModal = ({ open, onClose }: { open: boolean, onClose: () => void }) => { return } const ConfigureAccountsModalContent = () => { const { control, getValues, setValue } = useFormContext() const { call } = useContext(FrappeContext) as FrappeConfig // const costCenterMapRef = useRef>({}) const partyMapRef = useRef>({}) const onPartyChange = (value: string, index: number) => { // Get the account for the party type if (value) { if (partyMapRef.current[value]) { setValue(`accounts.${index}.account`, partyMapRef.current[value]) } else { call.get('erpnext.accounts.party.get_party_account', { party: value, party_type: getValues(`accounts.${index}.party_type`), company: company }).then((result: { message: string }) => { setValue(`accounts.${index}.account`, result.message) partyMapRef.current[value] = result.message }) } } else { setValue(`accounts.${index}.account`, '') } } const transaction_type = useWatch({ name: 'transaction_type', control, }) const { fields, append, remove } = useFieldArray({ control, name: 'accounts' }) const [selectedRows, setSelectedRows] = useState([]) const onSelectRow = useCallback((index: number) => { setSelectedRows(prev => { if (prev.includes(index)) { return prev.filter(i => i !== index) } return [...prev, index] }) }, []) const onSelectAll = useCallback(() => { setSelectedRows(prev => { if (prev.length === fields.length) { return [] } return [...fields.map((_, index) => index)] }) }, [fields]) const onAdd = () => { append({ party_type: '', party: '', account: '', debit: '', credit: '', user_remark: '' } as BankTransactionRuleAccounts, { focusName: `accounts.${fields.length}.account` }) } const onRemove = useCallback(() => { remove(selectedRows) setSelectedRows([]) }, [remove, selectedRows]) const isWithdrawal = transaction_type === 'Withdrawal' const company = useWatch({ name: 'company', control, }) return <> {_("Configure Accounts for Bank Entry")} {_("Add all accounts that you want to split the transaction into.")}
0 && selectedRows.length === fields.length} onCheckedChange={onSelectAll} /> {_("Party")} {_("Account")} * {/* {_("Cost Center")} */} {_("Remarks")} {_("Debit")} {_("Credit")} Bank GL Account {transaction_type === "Withdrawal" || transaction_type === "Any" ? _("Will be auto-populated") : ""} {transaction_type === "Deposit" || transaction_type === "Any" ? _("Will be auto-populated") : ""} {fields.map((field, index) => ( onSelectRow(index)} // Make this accessible to screen readers aria-label={_("Select row {0}", [String(index + 1)])} />
{ // onAccountChange(event.target.value, index) // } }} buttonClassName="min-w-64" isRequired hideLabel /> {/* */}
))}
{selectedRows.length > 0 &&
}

{_("Help")}

{(_("You can set up the rule to split the transaction across multiple accounts."))}
{_("You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25).")}

{_("Example")}:
transaction_amount * 0.25
{_("In this case, the amount will be calculated as 25% of the transaction amount. If the transaction amount is 200, then this will be calculated as 200 * 0.25 = 50.")}
} const PartyRowField = ({ index, onChange }: { index: number, onChange: (value: string, index: number) => void }) => { const { control } = useFormContext() const party_type = useWatch({ control, name: `accounts.${index}.party_type` }) if (!party_type) { return } return { onChange(event.target.value, index) }, }} hideLabel buttonClassName="rounded-s-none border-s-0 min-w-64" doctype={party_type} /> }