Load more transactions button

This commit is contained in:
Víctor Falcón 2025-11-14 13:02:42 +01:00
parent d13ecc2722
commit 7f243bbc1e
16 changed files with 82 additions and 78 deletions

View File

@ -27,3 +27,4 @@ class AccountSyncController extends Controller
}
}

View File

@ -28,3 +28,4 @@ class BankSyncController extends Controller
}
}

View File

@ -25,3 +25,4 @@ class CategorySyncController extends Controller
}
}

View File

@ -45,7 +45,7 @@ export function CreateAutomationRuleDialog({
const [open, setOpen] = useState(false);
const [categories, setCategories] = useState<Category[]>([]);
const [title, setTitle] = useState('');
const [priority, setPriority] = useState('0');
const [priority, setPriority] = useState('10');
const [ruleStructure, setRuleStructure] = useState<RuleStructure>({
groups: [createEmptyGroup()],
groupOperator: 'and',
@ -123,7 +123,7 @@ export function CreateAutomationRuleDialog({
onSuccess: async () => {
setOpen(false);
setTitle('');
setPriority('0');
setPriority('10');
setRuleStructure({
groups: [createEmptyGroup()],
groupOperator: 'and',
@ -186,7 +186,7 @@ export function CreateAutomationRuleDialog({
min="0"
value={priority}
onChange={(e) => setPriority(e.target.value)}
placeholder="0"
placeholder="10"
required
/>
<p className="text-xs text-muted-foreground">

View File

@ -75,3 +75,4 @@ export function SyncStatusButton() {
);
}

View File

@ -105,3 +105,4 @@ export function CategorySelect({
);
}

View File

@ -5,15 +5,17 @@ interface DataTablePaginationProps {
rowCountLabel?: string;
displayedCount?: number;
total?: number;
children?: React.ReactNode;
}
export function DataTablePagination({
displayedCount = undefined,
total = undefined,
rowCountLabel = 'row(s) total',
children,
}: DataTablePaginationProps) {
return (
<div className="flex px-5 items-center justify-end space-x-2">
<div className="flex items-center justify-end space-x-2">
{displayedCount && total && (
<div className="text-muted-foreground flex-1 text-sm">
{Math.min(total, displayedCount)} of {total} {rowCountLabel}
@ -24,15 +26,7 @@ export function DataTablePagination({
{displayedCount} {rowCountLabel}
</div>
)}
{((displayedCount ?? 0) < (total ?? 0)) && <div className="space-x-2">
<Button
variant="ghost"
size="sm"
disabled
>
Loading <RefreshCw className="ml-2 h-4 w-4 animate-spin" />
</Button>
</div>}
{children}
</div>
);
}

View File

@ -22,3 +22,4 @@ export function useOnlineStatus() {
return isOnline;
}

View File

@ -108,7 +108,12 @@ export default function AutomationRules() {
})),
[rawRules],
);
const [sorting, setSorting] = useState<SortingState>([]);
const [sorting, setSorting] = useState<SortingState>([
{
id: 'priority',
desc: false,
},
]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
{},
@ -116,23 +121,22 @@ export default function AutomationRules() {
const columns: ColumnDef<AutomationRule>[] = [
{
accessorKey: 'title',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
Title
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
accessorKey: 'priority',
header: 'Priority',
cell: ({ row }) => {
return (
<div className="pl-3 font-medium">
<div className="font-medium">
{row.getValue('priority')}
</div>
);
},
},
{
accessorKey: 'title',
header: 'Title',
cell: ({ row }) => {
return (
<div className="font-medium">
{row.getValue('title')}
</div>
);
@ -256,12 +260,12 @@ export default function AutomationRules() {
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext(),
)}
header
.column
.columnDef
.header,
header.getContext(),
)}
</TableHead>
);
},

View File

@ -10,7 +10,7 @@ import {
} from '@tanstack/react-table';
import { isWithinInterval, parseISO } from 'date-fns';
import { useLiveQuery } from 'dexie-react-hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { index as transactionsIndex } from '@/actions/App/Http/Controllers/TransactionController';
@ -30,10 +30,12 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/ui/data-table';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
import { Skeleton } from '@/components/ui/skeleton';
import { Spinner } from '@/components/ui/spinner';
import { useEncryptionKey } from '@/contexts/encryption-key-context';
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
import { decrypt, encrypt, importKey } from '@/lib/crypto';
@ -50,6 +52,7 @@ import {
type DecryptedTransaction,
type TransactionFilters as Filters,
} from '@/types/transaction';
import { ChevronDown } from 'lucide-react';
const breadcrumbs: BreadcrumbItem[] = [
{
@ -125,7 +128,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
const [isReEvaluating, setIsReEvaluating] = useState(false);
const [displayedCount, setDisplayedCount] = useState(25);
const observerTarget = useRef<HTMLDivElement>(null);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const updateTransaction = useCallback(
(updatedTransaction: DecryptedTransaction) => {
@ -552,7 +555,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
const selectedCategory = result.categoryId
? categories.find((c) => c.id === result.categoryId) ||
null
null
: null;
let decryptedNotes = transaction.decryptedNotes;
@ -696,7 +699,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
const selectedCategory = result.categoryId
? categories.find((c) => c.id === result.categoryId) ||
null
null
: null;
let decryptedNotes = transaction.decryptedNotes;
@ -805,34 +808,18 @@ export default function Transactions({ categories, accounts, banks }: Props) {
});
const loadMore = useCallback(() => {
if (displayedCount < sortedTransactions.length) {
setDisplayedCount((prev) =>
Math.min(prev + 25, sortedTransactions.length),
);
if (displayedCount < sortedTransactions.length && !isLoadingMore) {
setIsLoadingMore(true);
requestAnimationFrame(() => {
setDisplayedCount((prev) =>
Math.min(prev + 25, sortedTransactions.length),
);
requestAnimationFrame(() => {
setIsLoadingMore(false);
});
});
}
}, [displayedCount, sortedTransactions.length]);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 },
);
const currentTarget = observerTarget.current;
if (currentTarget) {
observer.observe(currentTarget);
}
return () => {
if (currentTarget) {
observer.unobserve(currentTarget);
}
};
}, [loadMore]);
}, [displayedCount, sortedTransactions.length, isLoadingMore]);
useEffect(() => {
setDisplayedCount(25);
@ -1031,18 +1018,25 @@ export default function Transactions({ categories, accounts, banks }: Props) {
displayedCount={displayedCount}
total={sortedTransactions.length}
rowCountLabel="transactions total"
/>
{displayedCount < sortedTransactions.length && (
<div
ref={observerTarget}
className="h-4 flex items-center justify-center"
>
<div className="text-sm text-muted-foreground">
Loading more...
</div>
</div>
)}
>
{displayedCount < sortedTransactions.length && (
<Button
onClick={loadMore}
disabled={isLoadingMore}
variant="outline"
>
{isLoadingMore ? (
<>
<Spinner />
Loading
</>
) : (<>
Load more
</>
)}
</Button>
)}
</DataTablePagination>
</>
)}
</div>

View File

@ -46,3 +46,4 @@ class AccountSyncService {
export const accountSyncService = new AccountSyncService();

View File

@ -42,3 +42,4 @@ class BankSyncService {
export const bankSyncService = new BankSyncService();

View File

@ -46,3 +46,4 @@ class CategorySyncService {
export const categorySyncService = new CategorySyncService();

View File

@ -61,3 +61,4 @@ export function formatRuleActions(rule: AutomationRule): string {
return 'Add note';
}

View File

@ -79,3 +79,4 @@ it('requires authentication', function () {
$response->assertUnauthorized();
});

View File

@ -53,3 +53,4 @@ it('requires authentication', function () {
$response->assertUnauthorized();
});