Added 200-page pagination

X-Lovable-Edit-ID: edt-6d77a572-8493-4558-8082-8bc375786ef0
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 20:05:57 +00:00
co-authored by renee-png
2 changed files with 58 additions and 15 deletions
-9
View File
@@ -1046,12 +1046,3 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+58 -6
View File
@@ -67,6 +67,9 @@ function InvoicesIndex() {
const [selectedExp, setSelectedExp] = useState<Set<string>>(new Set());
const [selectedInvoices, setSelectedInvoices] = useState<Set<string>>(new Set());
const [working, setWorking] = useState(false);
const [timePage, setTimePage] = useState(0);
const [expPage, setExpPage] = useState(0);
const TAB_PAGE_SIZE = 200;
// Map of placeholder invoice id -> true (placeholder = MARKED-INVOICED-* or IMPORT-PREBILLED-*)
const [placeholderIds, setPlaceholderIds] = useState<Set<string>>(new Set());
@@ -207,6 +210,21 @@ function InvoicesIndex() {
[visibleExp],
);
// Reset to page 0 when filter or underlying data changes
useEffect(() => { setTimePage(0); }, [timeFilter, time.length]);
useEffect(() => { setExpPage(0); }, [expFilter, expenses.length]);
const timeTotalPages = Math.max(1, Math.ceil(visibleTime.length / TAB_PAGE_SIZE));
const expTotalPages = Math.max(1, Math.ceil(visibleExp.length / TAB_PAGE_SIZE));
const pagedTime = useMemo(
() => visibleTime.slice(timePage * TAB_PAGE_SIZE, (timePage + 1) * TAB_PAGE_SIZE),
[visibleTime, timePage],
);
const pagedExp = useMemo(
() => visibleExp.slice(expPage * TAB_PAGE_SIZE, (expPage + 1) * TAB_PAGE_SIZE),
[visibleExp, expPage],
);
const toggleSel = (set: Set<string>, id: string) => {
const next = new Set(set);
if (next.has(id)) next.delete(id); else next.add(id);
@@ -455,8 +473,13 @@ function InvoicesIndex() {
<tr>
<th className="px-3 py-3 w-9">
<Checkbox
checked={visibleTime.length > 0 && selectedTime.size === visibleTime.length}
onCheckedChange={(c) => setSelectedTime(c ? new Set(visibleTime.map((t) => t.id)) : new Set())}
checked={pagedTime.length > 0 && pagedTime.every((t) => selectedTime.has(t.id))}
onCheckedChange={(c) => setSelectedTime((prev) => {
const next = new Set(prev);
if (c) pagedTime.forEach((t) => next.add(t.id));
else pagedTime.forEach((t) => next.delete(t.id));
return next;
})}
/>
</th>
<th className="text-left px-4 py-3 font-medium">Date</th>
@@ -477,7 +500,7 @@ function InvoicesIndex() {
No matching time entries.
</td></tr>
)}
{visibleTime.map((t) => {
{pagedTime.map((t) => {
const amount = Number(t.hours) * Number(t.hourly_rate);
const marked = isPlaceholder(t.invoice_id);
return (
@@ -522,6 +545,18 @@ function InvoicesIndex() {
</table>
</CardContent>
</Card>
{visibleTime.length > TAB_PAGE_SIZE && (
<div className="flex items-center justify-between mt-3 text-sm">
<span className="text-muted-foreground">
Showing {timePage * TAB_PAGE_SIZE + 1}–{Math.min((timePage + 1) * TAB_PAGE_SIZE, visibleTime.length)} of {visibleTime.length}
</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled={timePage === 0} onClick={() => setTimePage((p) => Math.max(0, p - 1))}>Previous</Button>
<span className="text-muted-foreground">Page {timePage + 1} of {timeTotalPages}</span>
<Button variant="outline" size="sm" disabled={timePage + 1 >= timeTotalPages} onClick={() => setTimePage((p) => p + 1)}>Next</Button>
</div>
</div>
)}
</TabsContent>
<TabsContent value="expenses" className="mt-4">
@@ -544,8 +579,13 @@ function InvoicesIndex() {
<tr>
<th className="px-3 py-3 w-9">
<Checkbox
checked={visibleExp.length > 0 && selectedExp.size === visibleExp.length}
onCheckedChange={(c) => setSelectedExp(c ? new Set(visibleExp.map((e) => e.id)) : new Set())}
checked={pagedExp.length > 0 && pagedExp.every((e) => selectedExp.has(e.id))}
onCheckedChange={(c) => setSelectedExp((prev) => {
const next = new Set(prev);
if (c) pagedExp.forEach((e) => next.add(e.id));
else pagedExp.forEach((e) => next.delete(e.id));
return next;
})}
/>
</th>
<th className="text-left px-4 py-3 font-medium">Date</th>
@@ -564,7 +604,7 @@ function InvoicesIndex() {
No matching expenses.
</td></tr>
)}
{visibleExp.map((e) => {
{pagedExp.map((e) => {
const marked = isPlaceholder(e.invoice_id);
return (
<tr key={e.id} className="border-t hover:bg-muted/30">
@@ -606,6 +646,18 @@ function InvoicesIndex() {
</table>
</CardContent>
</Card>
{visibleExp.length > TAB_PAGE_SIZE && (
<div className="flex items-center justify-between mt-3 text-sm">
<span className="text-muted-foreground">
Showing {expPage * TAB_PAGE_SIZE + 1}–{Math.min((expPage + 1) * TAB_PAGE_SIZE, visibleExp.length)} of {visibleExp.length}
</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" disabled={expPage === 0} onClick={() => setExpPage((p) => Math.max(0, p - 1))}>Previous</Button>
<span className="text-muted-foreground">Page {expPage + 1} of {expTotalPages}</span>
<Button variant="outline" size="sm" disabled={expPage + 1 >= expTotalPages} onClick={() => setExpPage((p) => p + 1)}>Next</Button>
</div>
</div>
)}
</TabsContent>
</Tabs>
</PageContainer>