Fix ImportStatementDialog overflow on narrow viewports #2

Merged
TehRiehlDeal merged 1 commits from feature/bulk-import-style-fix into main 2026-05-27 15:55:16 -07:00
3 changed files with 202 additions and 56 deletions
Showing only changes of commit 062b807732 - Show all commits
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tehriehlbudget-backend",
"version": "0.4.0",
"version": "0.4.1",
"description": "",
"author": "",
"private": true,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "tehriehlbudget-frontend",
"private": true,
"version": "0.4.0",
"version": "0.4.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -335,7 +335,7 @@ export function ImportStatementDialog({ open, onOpenChange, defaultAccountId, on
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-3xl">
<DialogContent className="sm:max-w-3xl md:max-w-5xl xl:max-w-6xl">
<DialogHeader>
<DialogTitle>Import statement</DialogTitle>
</DialogHeader>
@@ -638,12 +638,12 @@ function ReviewStep({
return (
<>
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
Review each row before import. Edit fields inline if needed; uncheck anything you don't
want to import.
</p>
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={onIncludeAll}>
Include all
</Button>
@@ -667,16 +667,17 @@ function ReviewStep({
)}
</div>
)}
<div className="max-h-[55vh] overflow-y-auto rounded-md border">
{/* Desktop / tablet: editable table with horizontal scroll fallback */}
<div className="hidden max-h-[55vh] overflow-auto rounded-md border md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8" />
<TableHead>Date</TableHead>
<TableHead className="w-[8.5rem]">Date</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead>Type</TableHead>
<TableHead>Status</TableHead>
<TableHead className="w-28 text-right">Amount</TableHead>
<TableHead className="w-[11rem]">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -685,7 +686,7 @@ function ReviewStep({
key={r.sourceIndex}
className={r.status === 'duplicate' ? 'bg-red-50/50' : undefined}
>
<TableCell>
<TableCell className="align-top">
<input
type="checkbox"
aria-label={`Include row ${r.sourceIndex + 1}`}
@@ -693,20 +694,25 @@ function ReviewStep({
onChange={(e) => onUpdateRow(r.sourceIndex, { included: e.target.checked })}
/>
</TableCell>
<TableCell>
<TableCell className="align-top">
<Input
type="date"
className="h-8 w-[8.5rem]"
className="h-8 w-[8rem]"
value={toDateInputValue(r.date)}
onChange={(e) => onUpdateRow(r.sourceIndex, { date: e.target.value })}
/>
</TableCell>
<TableCell>
<Input
className="h-8 min-w-[12rem]"
value={r.description}
onChange={(e) => onUpdateRow(r.sourceIndex, { description: e.target.value })}
/>
<TableCell className="align-top">
<div className="flex flex-wrap items-center gap-2">
<Input
className="h-8 min-w-0 flex-1"
value={r.description}
onChange={(e) =>
onUpdateRow(r.sourceIndex, { description: e.target.value })
}
/>
{statusBadge(r.status)}
</div>
{r.duplicateOf && (
<p className="mt-1 text-xs text-muted-foreground">
Matches existing {formatDate(r.duplicateOf.date)} ·{' '}
@@ -720,7 +726,7 @@ function ReviewStep({
</p>
)}
</TableCell>
<TableCell className="text-right">
<TableCell className="align-top text-right">
<Input
type="number"
step="0.01"
@@ -733,7 +739,7 @@ function ReviewStep({
}
/>
</TableCell>
<TableCell>
<TableCell className="align-top">
<Select
value={r.type}
onValueChange={(v) => {
@@ -747,7 +753,7 @@ function ReviewStep({
});
}}
>
<SelectTrigger className="h-8 w-[7rem]">
<SelectTrigger className="h-8 w-full">
<SelectValue>
{(v: string | undefined) =>
(v ?? 'EXPENSE').charAt(0) + (v ?? 'EXPENSE').slice(1).toLowerCase()
@@ -769,7 +775,7 @@ function ReviewStep({
})
}
>
<SelectTrigger className="mt-1 h-8 w-[10rem]">
<SelectTrigger className="mt-1 h-8 w-full">
<SelectValue placeholder="To account">
{(v: string | undefined) =>
accounts.find((a) => a.id === v)?.name ?? 'To account'
@@ -786,12 +792,126 @@ function ReviewStep({
</Select>
)}
</TableCell>
<TableCell>{statusBadge(r.status)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Mobile: stacked cards. Tables don't fit on a phone screen. */}
<div className="max-h-[60vh] space-y-2 overflow-y-auto md:hidden">
{rows.map((r) => (
<div
key={r.sourceIndex}
className={`rounded-md border p-3 ${
r.status === 'duplicate' ? 'bg-red-50/50' : 'bg-card'
}`}
>
<div className="flex items-start gap-2">
<input
type="checkbox"
aria-label={`Include row ${r.sourceIndex + 1}`}
checked={r.included}
onChange={(e) => onUpdateRow(r.sourceIndex, { included: e.target.checked })}
className="mt-2"
/>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<Input
type="date"
className="h-8 w-[8.5rem]"
value={toDateInputValue(r.date)}
onChange={(e) => onUpdateRow(r.sourceIndex, { date: e.target.value })}
/>
{statusBadge(r.status)}
</div>
<Input
className="h-8 w-full"
value={r.description}
onChange={(e) => onUpdateRow(r.sourceIndex, { description: e.target.value })}
/>
<div className="flex flex-wrap gap-2">
<Input
type="number"
step="0.01"
className="h-8 w-28 text-right"
value={r.amount}
onChange={(e) =>
onUpdateRow(r.sourceIndex, {
amount: parseFloat(e.target.value) || 0,
})
}
/>
<Select
value={r.type}
onValueChange={(v) => {
const newType = (v ?? 'EXPENSE') as RowType;
onUpdateRow(r.sourceIndex, {
type: newType,
destinationAccountId:
newType === 'TRANSFER'
? (r.destinationAccountId ?? r.transferCandidate?.accountId ?? '')
: undefined,
});
}}
>
<SelectTrigger className="h-8 w-[7.5rem]">
<SelectValue>
{(v: string | undefined) =>
(v ?? 'EXPENSE').charAt(0) + (v ?? 'EXPENSE').slice(1).toLowerCase()
}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="INCOME">Income</SelectItem>
<SelectItem value="EXPENSE">Expense</SelectItem>
<SelectItem value="TRANSFER">Transfer</SelectItem>
</SelectContent>
</Select>
</div>
{r.type === 'TRANSFER' && (
<Select
value={r.destinationAccountId ?? ''}
onValueChange={(v) =>
onUpdateRow(r.sourceIndex, {
destinationAccountId: v ?? '',
})
}
>
<SelectTrigger className="h-8 w-full">
<SelectValue placeholder="To account">
{(v: string | undefined) =>
accounts.find((a) => a.id === v)?.name ?? 'To account'
}
</SelectValue>
</SelectTrigger>
<SelectContent>
{otherAccounts.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{r.duplicateOf && (
<p className="text-xs text-muted-foreground">
Matches existing {formatDate(r.duplicateOf.date)} ·{' '}
{currency(r.duplicateOf.amount)} · {r.duplicateOf.description}
</p>
)}
{r.transferCandidate && !r.destinationAccountId && (
<p className="text-xs text-sky-900">
Possible transfer with {r.transferCandidate.accountName}. Pick a destination
account above to mark as transfer.
</p>
)}
</div>
</div>
</div>
))}
</div>
<p className="text-xs text-muted-foreground">
{counts.selected} selected · {counts.duplicates} duplicates · {counts.needsReview} need
review · {counts.possibleTransfers} possible transfers
@@ -902,39 +1022,65 @@ function ConfirmStep({
{counts.selected === 1 ? '' : 's'} about to be imported
</button>
{expanded && (
<div className="max-h-[40vh] overflow-y-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead>Type</TableHead>
<TableHead>Account</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedRows.map((r) => (
<TableRow key={r.sourceIndex}>
<TableCell>{formatDate(r.date)}</TableCell>
<TableCell>{r.description}</TableCell>
<TableCell className="text-right">{currency(r.amount)}</TableCell>
<TableCell>{r.type.charAt(0) + r.type.slice(1).toLowerCase()}</TableCell>
<TableCell>
{sourceAccount?.name ?? '—'}
{r.destinationAccountId && (
<>
{' '}
<ArrowRight className="inline size-3" />{' '}
{accounts.find((a) => a.id === r.destinationAccountId)?.name ?? '—'}
</>
)}
</TableCell>
<>
{/* Desktop / tablet: read-only summary table */}
<div className="hidden max-h-[40vh] overflow-auto rounded-md border md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[7.5rem]">Date</TableHead>
<TableHead>Description</TableHead>
<TableHead className="w-28 text-right">Amount</TableHead>
<TableHead className="w-24">Type</TableHead>
<TableHead>Account</TableHead>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
{selectedRows.map((r) => (
<TableRow key={r.sourceIndex}>
<TableCell>{formatDate(r.date)}</TableCell>
<TableCell>{r.description}</TableCell>
<TableCell className="text-right">{currency(r.amount)}</TableCell>
<TableCell>{r.type.charAt(0) + r.type.slice(1).toLowerCase()}</TableCell>
<TableCell>
{sourceAccount?.name ?? '—'}
{r.destinationAccountId && (
<>
{' '}
<ArrowRight className="inline size-3" />{' '}
{accounts.find((a) => a.id === r.destinationAccountId)?.name ?? '—'}
</>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Mobile: stacked cards */}
<div className="max-h-[40vh] space-y-2 overflow-y-auto md:hidden">
{selectedRows.map((r) => (
<div key={r.sourceIndex} className="rounded-md border bg-card p-3 text-sm">
<div className="flex items-baseline justify-between gap-2">
<span className="font-medium">{r.description}</span>
<span className="whitespace-nowrap">{currency(r.amount)}</span>
</div>
<p className="mt-1 text-xs text-muted-foreground">
{formatDate(r.date)} · {r.type.charAt(0) + r.type.slice(1).toLowerCase()} ·{' '}
{sourceAccount?.name ?? '—'}
{r.destinationAccountId && (
<>
{' '}
<ArrowRight className="inline size-3" />{' '}
{accounts.find((a) => a.id === r.destinationAccountId)?.name ?? '—'}
</>
)}
</p>
</div>
))}
</div>
</>
)}
{error && <p className="text-sm text-destructive">{error}</p>}