mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 18:21:22 +00:00
fix: UI/UX issues in new banking module (#54824)
* fix: enforce user permissions on bank account get_list * feat: auto-select last used bank account * fix: skeleton loaders in bank balance * fix: show empty state for no bank transactions * chore: add Stripe and PayPal logos * fix: alignment of header text in list-view * fix: wrap words in transaction description * fix: change file-dropzone color on hover
This commit is contained in:
@@ -53,7 +53,7 @@ const OpeningBalance = () => {
|
||||
|
||||
return <StatContainer className="min-w-48">
|
||||
<StatLabel>{_("Opening Balance")}</StatLabel>
|
||||
{isLoading ? <Skeleton className="w-[150px] h-9" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
</StatContainer>
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ const ClosingBalance = () => {
|
||||
</HoverCard>
|
||||
|
||||
</div>
|
||||
{isLoading ? <Skeleton className="w-[150px] h-9" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
</StatContainer>
|
||||
)
|
||||
}
|
||||
@@ -104,7 +104,7 @@ const Difference = () => {
|
||||
|
||||
return <StatContainer className="w-fit text-end sm:min-w-56">
|
||||
<StatLabel className="text-end">{_("Difference")}</StatLabel>
|
||||
{isLoading ? <Skeleton className="w-[150px] h-9" /> : <StatValue className={isError ? 'text-ink-red-3 font-numeric' : 'font-numeric'}>
|
||||
{isLoading ? <Skeleton className="w-[150px] h-5 self-end rounded-sm" /> : <StatValue className={isError ? 'text-ink-red-3 font-numeric' : 'font-numeric'}>
|
||||
{formatCurrency(difference,
|
||||
bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))
|
||||
}</StatValue>}
|
||||
@@ -175,7 +175,7 @@ const ClosingBalanceAsPerStatement = () => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-4 underline cursor-pointer underline-offset-6" role="button">
|
||||
{isLoading ? <Skeleton className="w-[150px] h-9" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message?.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message?.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
|
||||
<Edit className="w-4 h-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAtom, useSetAtom } from "jotai"
|
||||
import { useAtom } from "jotai"
|
||||
import { SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
|
||||
import { useCallback } from "react"
|
||||
import { useGetBankAccounts, useGetUnreconciledTransactions } from "./utils"
|
||||
@@ -16,9 +16,16 @@ import { useCurrentCompany } from "@/hooks/useCurrentCompany"
|
||||
|
||||
const BankPicker = ({ className }: { className?: string }) => {
|
||||
|
||||
const setSelectedBank = useSetAtom(selectedBankAccountAtom)
|
||||
const [selectedBank, setSelectedBank] = useAtom(selectedBankAccountAtom)
|
||||
|
||||
const onLoadingSuccess = useCallback((data?: SelectedBank[]) => {
|
||||
// If the bank is already selected, then don't set it again
|
||||
if (selectedBank) {
|
||||
// Check if selected bank is in the data
|
||||
if (data?.some((bank: SelectedBank) => bank.name === selectedBank.name)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!data) return
|
||||
if (data.length === 1) {
|
||||
setSelectedBank(data[0])
|
||||
@@ -26,9 +33,12 @@ const BankPicker = ({ className }: { className?: string }) => {
|
||||
const defaultBank = data.find((bank: SelectedBank) => bank.is_default)
|
||||
if (defaultBank) {
|
||||
setSelectedBank(defaultBank)
|
||||
} else {
|
||||
// Select the first available bank account
|
||||
setSelectedBank(data[0])
|
||||
}
|
||||
}
|
||||
}, [setSelectedBank])
|
||||
}, [setSelectedBank, selectedBank])
|
||||
|
||||
const selectedCompany = useCurrentCompany()
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useDebounceValue } from "usehooks-ts"
|
||||
import type { ColumnDef } from "@tanstack/react-table"
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { Link } from "react-router"
|
||||
import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription } from "@/components/ui/empty"
|
||||
import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription, EmptyContent } from "@/components/ui/empty"
|
||||
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
|
||||
|
||||
const BankTransactions = () => {
|
||||
@@ -262,7 +262,7 @@ const BankTransactionListView = () => {
|
||||
|
||||
{error && <ErrorBanner error={error} />}
|
||||
|
||||
{data && data.message.length > 0 && <Filters
|
||||
<Filters
|
||||
onSearchChange={onSearchChange}
|
||||
search={search}
|
||||
results={filteredResults}
|
||||
@@ -272,28 +272,31 @@ const BankTransactionListView = () => {
|
||||
typeFilter={typeFilter}
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
/>}
|
||||
|
||||
{data && data.message.length > 0 ? (
|
||||
<ListView
|
||||
data={filteredResults}
|
||||
columns={transactionColumns}
|
||||
getRowId={(row) => row.name}
|
||||
maxHeight="calc(100vh - 200px)"
|
||||
scrollAreaClassName="min-h-[calc(100vh-200px)]"
|
||||
emptyState={<Empty>
|
||||
<EmptyMedia>
|
||||
<ListIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>{_("No bank transactions found")}</EmptyTitle>
|
||||
<EmptyDescription>{_("There are no transactions in the system for the selected bank account and dates that match the filters.")}</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
/>
|
||||
|
||||
<ListView
|
||||
data={filteredResults}
|
||||
columns={transactionColumns}
|
||||
getRowId={(row) => row.name}
|
||||
maxHeight="calc(100vh - 200px)"
|
||||
scrollAreaClassName="min-h-[calc(100vh-200px)]"
|
||||
emptyState={<Empty>
|
||||
<EmptyMedia>
|
||||
<ListIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>{_("No bank transactions found")}</EmptyTitle>
|
||||
<EmptyDescription>{_("There are no transactions in the system for the selected bank account and dates that match the filters.")}</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
{data && data.message.length === 0 ? <EmptyContent>
|
||||
<Button type='button' asChild variant='outline'>
|
||||
<Link to="/statement-importer">
|
||||
{_("Import Bank Statement")}
|
||||
</Link>
|
||||
</Button>
|
||||
</EmptyContent> : null}
|
||||
</Empty>}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ const UnreconciledTransactionItem = ({ transaction }: { transaction: Unreconcile
|
||||
tabIndex={0}
|
||||
onClick={handleSelectTransaction}>
|
||||
<div className="flex justify-between items-start w-full">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 overflow-hidden whitespace-pre-wrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium text-sm">{formatDate(transaction.date)}</span>
|
||||
{transaction.transaction_type &&
|
||||
@@ -314,7 +314,7 @@ const UnreconciledTransactionItem = ({ transaction }: { transaction: Unreconcile
|
||||
title={_("Matched by rule")}>
|
||||
<ZapIcon className="w-4 h-4" /> {transaction.matched_transaction_rule}</Badge>}
|
||||
</div>
|
||||
<span className="text-sm">{transaction.description}</span>
|
||||
<span className="text-sm wrap-anywhere" title={transaction.description}>{transaction.description}</span>
|
||||
</div>
|
||||
<div className="gap-1 flex flex-col items-end min-w-36 h-full text-end">
|
||||
{isWithdrawal ? <ArrowUpRight className="size-5 text-ink-red-3" /> : <ArrowDownRight className="size-5 text-ink-green-3" />}
|
||||
|
||||
@@ -15,7 +15,9 @@ export interface SelectedBank extends Pick<BankAccount, 'name' | 'bank' | 'is_cr
|
||||
logoClassName?: string,
|
||||
account_currency?: string
|
||||
}
|
||||
export const selectedBankAccountAtom = atom<SelectedBank | null>(null)
|
||||
export const selectedBankAccountAtom = atomWithStorage<SelectedBank | null>('bank-rec-selected-bank', null, undefined, {
|
||||
getOnInit: true
|
||||
})
|
||||
|
||||
export const bankRecDateAtom = atomWithStorage<{ fromDate: string, toDate: string }>("bank-rec-date", {
|
||||
fromDate: getDatesForTimePeriod('This Month').fromDate,
|
||||
|
||||
@@ -393,5 +393,18 @@ export const BANK_LOGOS: { keywords: string[], logo: string, locale?: string[],
|
||||
logo: "Prime_Bank.png",
|
||||
locale: ['Kenya'],
|
||||
logoClassName: 'max-w-28'
|
||||
},
|
||||
{
|
||||
keywords: ["Stripe"],
|
||||
logo: "Stripe.svg",
|
||||
locale: ['Global'],
|
||||
logoClassName: 'h-6',
|
||||
darkModeInvert: true,
|
||||
},
|
||||
{
|
||||
keywords: ["PayPal"],
|
||||
logo: "PayPal.png",
|
||||
locale: ['Global'],
|
||||
logoClassName: 'h-6',
|
||||
}
|
||||
]
|
||||
@@ -31,7 +31,7 @@ export const FileDropzone = ({ files, setFiles, accept, multiple = true, onDrop,
|
||||
}, [setFiles, onDrop, multiple, onUpdate])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop: onFileDrop, accept, multiple })
|
||||
return (
|
||||
<div {...getRootProps()} className={cn('border border-outline-gray-2 border-dashed p-4 rounded bg-surface-gray-1 focus-within:bg-surface-gray-2 focus-within:border-outline-gray-4 focus-within:outline-none', className)}>
|
||||
<div {...getRootProps()} className={cn('border border-outline-gray-2 border-dashed p-4 rounded bg-surface-gray-1 focus-within:bg-surface-gray-2 hover:bg-surface-gray-2 hover:border-outline-gray-3 focus-within:border-outline-gray-3 focus-within:outline-none', className)}>
|
||||
<input {...getInputProps()} />
|
||||
{files.length === 0 ? <p className='text-sm text-ink-gray-5 text-center h-8 flex items-center justify-center'>{multiple ? _("Drop some files here, or click to select files") : _("Drop a file here, or click to select a file")}</p> : null}
|
||||
<div className='flex flex-col gap-4'>
|
||||
|
||||
@@ -382,7 +382,7 @@ function ListViewInner<TData>({
|
||||
<div
|
||||
key={header.id}
|
||||
className={cn(
|
||||
"text-ink-gray-5 group relative flex min-w-0 items-center px-1 text-sm",
|
||||
"text-ink-gray-5 group relative flex min-w-0 items-center px-0 text-sm",
|
||||
alignClass(meta),
|
||||
)}
|
||||
role="columnheader"
|
||||
@@ -397,7 +397,7 @@ function ListViewInner<TData>({
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute ltr:right-0 rtl:left-0 z-1 w-0.5 bg-gray-400",
|
||||
"pointer-events-none absolute ltr:-right-2 rtl:-left-2 z-1 w-0.5 bg-gray-400",
|
||||
"opacity-0 transition-[opacity,background-color] ease-in-out duration-150",
|
||||
"group-hover:opacity-100 group-hover:bg-gray-400",
|
||||
header.column.getIsResizing() && "bg-outline-gray-6 opacity-100",
|
||||
@@ -421,7 +421,7 @@ function ListViewInner<TData>({
|
||||
header.getResizeHandler()(e)
|
||||
}}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className="absolute top-0 ltr:right-0 rtl:left-0 z-10 h-full w-2 max-w-[12px] cursor-col-resize touch-none select-none bg-transparent"
|
||||
className="absolute top-0 ltr:-right-2 rtl:-left-2 z-10 h-full w-2 max-w-[12px] cursor-col-resize touch-none select-none bg-transparent"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user