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")}
remove(index)} disabled={fields.length === 1}>
))}
)
}
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
{_("Journal Template Accounts")}*
setIsConfigureAccountsModalOpen(true)}> {_("Configure Accounts")}
{_("Account")}
{_("Debit")}
{_("Credit")}
{accounts.length === 0 && (
{_("No accounts configured")}
setIsConfigureAccountsModalOpen(true)}>{_("Configure Accounts")}
)}
{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 &&
{_("Remove")}
}
{_("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}
/>
}