Compare commits
2 Commits
0284923d5e
...
2c6db4b0a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c6db4b0a1 | |||
| 062b807732 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tehriehlbudget-backend",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user