fix(ux): improve status badge, hide balance update for connected accounts, localize delete confirm (#159)
## Why ### Problem Three UX issues were identified: 1. **Connection status badge looked like a button** — the `<Badge>` component has interactive-looking styles that made it visually ambiguous as a status indicator rather than a purely informational element. 2. **"Update balance" was shown for bank-connected accounts** — accounts synced via Open Banking have their balances managed automatically; showing a manual update button was misleading and could create confusion. 3. **Delete confirmation word was not localized** — the dialog required typing `DELETE` even when the UI was in Spanish, where the placeholder correctly said `ELIMINAR`. The check was hardcoded to `'DELETE'`. ## What ### Changes <img width="756" height="407" alt="image" src="https://github.com/user-attachments/assets/4866edf7-5329-486a-9f72-fcd69429f784" /> <img width="757" height="424" alt="image" src="https://github.com/user-attachments/assets/bc9bda54-e0a4-402b-be32-a9fe1897cfd5" />
This commit is contained in:
parent
a4d2100459
commit
79dd24b23e
|
|
@ -24,7 +24,7 @@ class AccountController extends Controller
|
|||
->with('bank:id,name,logo')
|
||||
->orderByRaw("FIELD(type, 'checking', 'savings', 'investment', 'retirement', 'loan', 'credit_card', 'others')")
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code']);
|
||||
->get(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id']);
|
||||
|
||||
return Inertia::render('Accounts/Index', [
|
||||
'accounts' => $accounts,
|
||||
|
|
@ -39,7 +39,7 @@ class AccountController extends Controller
|
|||
$account->load('bank:id,name,logo');
|
||||
|
||||
return Inertia::render('Accounts/Show', [
|
||||
'account' => $account->only(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'bank']),
|
||||
'account' => $account->only(['id', 'name', 'name_iv', 'encrypted', 'bank_id', 'type', 'currency_code', 'banking_connection_id', 'bank']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ class AccountMetricsService
|
|||
'type' => $account->type,
|
||||
'currency_code' => $account->currency_code,
|
||||
'bank' => $account->bank,
|
||||
'banking_connection_id' => $account->banking_connection_id,
|
||||
];
|
||||
|
||||
if ($account->type->supportsInvestedAmount()) {
|
||||
|
|
|
|||
2094
lang/es.json
2094
lang/es.json
File diff suppressed because it is too large
Load Diff
|
|
@ -52,6 +52,7 @@ export function AccountListCard({
|
|||
}
|
||||
|
||||
const isPositive = account.diff >= 0;
|
||||
const isConnected = !!account.banking_connection_id;
|
||||
|
||||
return (
|
||||
<Card className="w-full py-0">
|
||||
|
|
@ -86,18 +87,29 @@ export function AccountListCard({
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
className="-mr-2 cursor-pointer rounded-md px-2 py-1 transition-colors hover:bg-muted"
|
||||
>
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="bold"
|
||||
/>
|
||||
</button>
|
||||
{isConnected ? (
|
||||
<div className="-mr-2 px-2 py-1">
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="bold"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
className="-mr-2 cursor-pointer rounded-md px-2 py-1 transition-colors hover:bg-muted"
|
||||
>
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="bold"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<AmountTrendIndicator
|
||||
isPositive={isPositive}
|
||||
trend={Math.abs(account.diff)}
|
||||
|
|
@ -176,7 +188,7 @@ export function AccountListCard({
|
|||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`whitespace-nowrap text-right font-mono tabular-nums ${gain >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
|
||||
className={`text-right font-mono whitespace-nowrap tabular-nums ${gain >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
|
||||
>
|
||||
{gain >= 0
|
||||
? '+'
|
||||
|
|
@ -232,15 +244,20 @@ export function AccountListCard({
|
|||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
variant="secondary"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
>
|
||||
{__('Update balance')}
|
||||
</Button>
|
||||
{!isConnected && (
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
variant="secondary"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
>
|
||||
{__('Update balance')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Link href={show.url(account.id)}>
|
||||
<Link
|
||||
href={show.url(account.id)}
|
||||
className={isConnected ? 'ml-auto' : ''}
|
||||
>
|
||||
<Button className="cursor-pointer" variant="ghost">
|
||||
{__('Details')} →
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export function DeleteAccountDialog({
|
|||
}: DeleteAccountDialogProps) {
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
|
||||
const confirmWord = __('DELETE');
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
setConfirmText('');
|
||||
|
|
@ -39,7 +41,7 @@ export function DeleteAccountDialog({
|
|||
onOpenChange(newOpen);
|
||||
}
|
||||
|
||||
const isDeleteEnabled = confirmText === 'DELETE';
|
||||
const isDeleteEnabled = confirmText === confirmWord;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
|
|
@ -49,14 +51,17 @@ export function DeleteAccountDialog({
|
|||
<DialogDescription className="space-y-2">
|
||||
<p>
|
||||
{__(
|
||||
'This action is irreversible. All transactions in\n this account will also be permanently deleted.',
|
||||
'This action is irreversible. All transactions in this account will also be permanently deleted.',
|
||||
)}
|
||||
</p>
|
||||
<p className="font-semibold">
|
||||
{__('Type')}
|
||||
<span className="text-red-600">DELETE</span>
|
||||
{__('to\n confirm.')}
|
||||
</p>
|
||||
<p
|
||||
className="font-semibold"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: __('Type :word to confirm.', {
|
||||
word: `<span class="text-red-600">${confirmWord}</span>`,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -99,7 +104,9 @@ export function DeleteAccountDialog({
|
|||
variant="destructive"
|
||||
disabled={processing || !isDeleteEnabled}
|
||||
>
|
||||
{processing ? 'Deleting...' : 'Delete'}
|
||||
{processing
|
||||
? __('Deleting...')
|
||||
: __('Delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export function AccountBalanceCard({
|
|||
}
|
||||
|
||||
const isPositive = account.diff >= 0;
|
||||
const isConnected = !!account.banking_connection_id;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -72,18 +73,29 @@ export function AccountBalanceCard({
|
|||
<CardContent>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
className="-ml-2 cursor-pointer rounded-md px-2 py-1 text-left transition-colors hover:bg-muted"
|
||||
>
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="medium"
|
||||
/>
|
||||
</button>
|
||||
{isConnected ? (
|
||||
<div className="-ml-2 px-2 py-1">
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="medium"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
className="-ml-2 cursor-pointer rounded-md px-2 py-1 text-left transition-colors hover:bg-muted"
|
||||
>
|
||||
<AmountDisplay
|
||||
amountInCents={account.currentBalance}
|
||||
currencyCode={account.currency_code}
|
||||
size="2xl"
|
||||
weight="medium"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
<AmountTrendIndicator
|
||||
isPositive={isPositive}
|
||||
trend={Math.abs(account.diff)}
|
||||
|
|
@ -161,7 +173,7 @@ export function AccountBalanceCard({
|
|||
)}
|
||||
</span>
|
||||
<span
|
||||
className={`whitespace-nowrap text-right font-mono tabular-nums ${gain >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
|
||||
className={`text-right font-mono whitespace-nowrap tabular-nums ${gain >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`}
|
||||
>
|
||||
{gain >= 0
|
||||
? '+'
|
||||
|
|
|
|||
|
|
@ -8,13 +8,37 @@ const statusConfig: Record<
|
|||
{
|
||||
label: string;
|
||||
variant: 'default' | 'secondary' | 'destructive' | 'outline';
|
||||
className?: string;
|
||||
}
|
||||
> = {
|
||||
active: { label: 'Active', variant: 'default' },
|
||||
awaiting_mapping: { label: 'Setup Required', variant: 'secondary' },
|
||||
pending: { label: 'Pending', variant: 'secondary' },
|
||||
expired: { label: 'Expired', variant: 'outline' },
|
||||
revoked: { label: 'Revoked', variant: 'outline' },
|
||||
active: {
|
||||
label: 'Active',
|
||||
variant: 'default',
|
||||
className:
|
||||
'bg-green-100 text-green-800 border-green-200 dark:bg-green-500/10 dark:text-green-400 dark:border-green-500/20',
|
||||
},
|
||||
awaiting_mapping: {
|
||||
label: 'Setup Required',
|
||||
variant: 'secondary',
|
||||
className:
|
||||
'dark:bg-yellow-900/60 dark:text-yellow-200 dark:border-yellow-700/50',
|
||||
},
|
||||
pending: {
|
||||
label: 'Pending',
|
||||
variant: 'secondary',
|
||||
className:
|
||||
'dark:bg-yellow-900/60 dark:text-yellow-200 dark:border-yellow-700/50',
|
||||
},
|
||||
expired: {
|
||||
label: 'Expired',
|
||||
variant: 'outline',
|
||||
className: 'dark:text-muted-foreground',
|
||||
},
|
||||
revoked: {
|
||||
label: 'Revoked',
|
||||
variant: 'outline',
|
||||
className: 'dark:text-muted-foreground',
|
||||
},
|
||||
error: { label: 'Error', variant: 'destructive' },
|
||||
};
|
||||
|
||||
|
|
@ -27,7 +51,10 @@ export function ConnectionStatusBadge({
|
|||
}) {
|
||||
if (status === 'active' && !lastSyncedAt) {
|
||||
return (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-1 border-green-200 bg-green-100 text-green-800 dark:border-yellow-700/50 dark:bg-yellow-900/60 dark:text-yellow-200"
|
||||
>
|
||||
<Spinner className="size-3" />
|
||||
{__('Syncing')}
|
||||
</Badge>
|
||||
|
|
@ -36,5 +63,9 @@ export function ConnectionStatusBadge({
|
|||
|
||||
const config = statusConfig[status];
|
||||
|
||||
return <Badge variant={config.variant}>{__(config.label)}</Badge>;
|
||||
return (
|
||||
<Badge variant={config.variant} className={config.className}>
|
||||
{__(config.label)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface NetWorthEvolutionAccount {
|
|||
type: AccountType;
|
||||
currency_code: string;
|
||||
bank: Bank;
|
||||
banking_connection_id: string | null;
|
||||
invested_amount?: number | null;
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ export function deriveAccountMetrics(
|
|||
type: account.type,
|
||||
currency_code: account.currency_code,
|
||||
bank: account.bank,
|
||||
banking_connection_id: account.banking_connection_id,
|
||||
currentBalance,
|
||||
previousBalance,
|
||||
diff: currentBalance - previousBalance,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export default function AccountShow({
|
|||
setChartRefreshKey((prev) => prev + 1);
|
||||
}
|
||||
|
||||
const isConnected = !!account.banking_connection_id;
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Accounts',
|
||||
|
|
@ -94,60 +96,75 @@ export default function AccountShow({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<ButtonGroup>
|
||||
{isConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
{__('Edit account')}
|
||||
</Button>
|
||||
) : (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
>
|
||||
{__('Update balance')}
|
||||
</Button>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setUpdateBalanceOpen(true)}
|
||||
>
|
||||
{__('Update balance')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setImportBalancesOpen(true)}
|
||||
>
|
||||
{__('Import balances')}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={__('More options')}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setBalancesOpen(true)
|
||||
}
|
||||
>
|
||||
{__('See balances')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
{__('Edit account')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
variant="destructive"
|
||||
>
|
||||
{__('Delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setImportBalancesOpen(true)}
|
||||
>
|
||||
{__('Import balances')}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label={__('More options')}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setBalancesOpen(true)}
|
||||
>
|
||||
{__('See balances')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditOpen(true)}
|
||||
>
|
||||
{__('Edit account')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
variant="destructive"
|
||||
>
|
||||
{__('Delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AccountBalanceChart
|
||||
account={account}
|
||||
refreshKey={chartRefreshKey}
|
||||
onBalanceClick={() => setUpdateBalanceOpen(true)}
|
||||
onBalanceClick={
|
||||
isConnected
|
||||
? undefined
|
||||
: () => setUpdateBalanceOpen(true)
|
||||
}
|
||||
/>
|
||||
|
||||
{isTransactionalAccount(account) && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue