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:
Nikhil Kothari
2026-05-11 13:02:11 +05:30
committed by GitHub
parent 03acbc3dc9
commit f4008adc16
11 changed files with 99 additions and 66 deletions

View File

@@ -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>

View File

@@ -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()

View File

@@ -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>
}

View File

@@ -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" />}

View File

@@ -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,

View File

@@ -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',
}
]

View File

@@ -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'>

View File

@@ -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}