From eae4857c9dbe159e9479e207848d7647495f2515 Mon Sep 17 00:00:00 2001 From: Deardo Satria Date: Sun, 1 Nov 2026 21:00:09 +0700 Subject: [PATCH] Add pagination to pelanggan list --- .../_components/daftar-pelanggan.tsx | 110 ++++++++++++++++++ app/(dashboard)/master/pelanggan/page.tsx | 66 +---------- components/pagination.tsx | 51 ++++++++ data/pelanggan.ts | 26 ++++- 4 files changed, 185 insertions(+), 68 deletions(-) create mode 100644 app/(dashboard)/master/pelanggan/_components/daftar-pelanggan.tsx create mode 100644 components/pagination.tsx diff --git a/app/(dashboard)/master/pelanggan/_components/daftar-pelanggan.tsx b/app/(dashboard)/master/pelanggan/_components/daftar-pelanggan.tsx new file mode 100644 index 0000000..bac284a --- /dev/null +++ b/app/(dashboard)/master/pelanggan/_components/daftar-pelanggan.tsx @@ -0,0 +1,110 @@ +import { PencilIcon } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Pagination } from "@/components/pagination"; +import { listPelanggan } from "@/data/pelanggan"; +import { HapusPelangganButton } from "./hapus-pelanggan-button"; +import { PelangganForm } from "./pelanggan-form"; + +export async function DaftarPelanggan({ + searchParams, +}: { + searchParams: Promise<{ search?: string; page?: string }>; +}) { + const { search, page: pageParam } = await searchParams; + const parsed = Number.parseInt(pageParam ?? "1", 10); + // Batas atas mencegah offset melebihi BIGINT MariaDB (query error). + const page = Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : 1; + const { rows, hasNext } = await listPelanggan(search, page); + + return ( + <> +
+
+ + + + + + + + + + + + + + {rows.map((p) => ( + + + + + + + + + + ))} + {rows.length === 0 && ( + + + + )} + +
KodeNamaAliasAlamatPonselBonAksi
+ {p.kdCustomer} + {p.nama} + {p.alias} + + {p.alamat} + + {p.ponsel ?? "-"} + + {p.nilaiBon !== null && p.nilaiBon > 0 ? p.nilaiBon : "-"} + +
+ + + Edit + + } + /> + +
+
+ {page > 1 ? "Halaman ini kosong" : "Tidak ada pelanggan"} +
+
+
+ halamanHref(p, search)} + /> + + ); +} + +function halamanHref(page: number, search?: string): string { + const params = new URLSearchParams(); + if (search) params.set("search", search); + if (page > 1) params.set("page", String(page)); + const query = params.toString(); + return query ? `/master/pelanggan?${query}` : "/master/pelanggan"; +} diff --git a/app/(dashboard)/master/pelanggan/page.tsx b/app/(dashboard)/master/pelanggan/page.tsx index ca18cbf..181df1d 100644 --- a/app/(dashboard)/master/pelanggan/page.tsx +++ b/app/(dashboard)/master/pelanggan/page.tsx @@ -1,17 +1,13 @@ import { Suspense } from "react"; -import { PencilIcon } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { listPelanggan } from "@/data/pelanggan"; -import { HapusPelangganButton } from "./_components/hapus-pelanggan-button"; -import { PelangganForm } from "./_components/pelanggan-form"; +import { DaftarPelanggan } from "./_components/daftar-pelanggan"; import { TambahPelangganButton } from "./_components/tambah-pelanggan-button"; import { CariPelanggan } from "./_components/cari-pelanggan"; export default function PelangganPage({ searchParams, }: { - searchParams: Promise<{ search?: string }>; + searchParams: Promise<{ search?: string; page?: string }>; }) { return (
@@ -30,61 +26,3 @@ export default function PelangganPage({
); } - -async function DaftarPelanggan({ - searchParams, -}: { - searchParams: Promise<{ search?: string }>; -}) { - const { search } = await searchParams; - const daftar = await listPelanggan(search); - - return ( - - - - - - - - - - - - - - {daftar.map((p) => ( - - - - - - - - - - ))} - {daftar.length === 0 && ( - - - - )} - -
KodeNamaAliasAlamatPonselBonAksi
{p.kdCustomer}{p.nama}{p.alias}{p.alamat}{p.ponsel ?? "-"} - {p.nilaiBon !== null && p.nilaiBon > 0 ? p.nilaiBon : "-"} - - - - Edit - - } - /> - -
- Tidak ada pelanggan -
- ); -} diff --git a/components/pagination.tsx b/components/pagination.tsx new file mode 100644 index 0000000..e9b6fc6 --- /dev/null +++ b/components/pagination.tsx @@ -0,0 +1,51 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { cn } from "cn"; + +import { buttonVariants } from "@/components/ui/button"; + +type PaginationProps = { + page: number; + hasNext: boolean; + hrefPage: (page: number) => string; +}; + +export function Pagination({ page, hasNext, hrefPage }: PaginationProps) { + return ( +
+ Halaman {page} +
+ 1 ? hrefPage(page - 1) : null}> + Sebelumnya + + + Berikutnya + +
+
+ ); +} + +function TombolHalaman({ + href, + children, +}: { + href: string | null; + children: ReactNode; +}) { + const className = buttonVariants({ variant: "outline", size: "sm" }); + + if (!href) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} diff --git a/data/pelanggan.ts b/data/pelanggan.ts index b88ca37..9a3bf7c 100644 --- a/data/pelanggan.ts +++ b/data/pelanggan.ts @@ -20,6 +20,13 @@ export type Pelanggan = { nilaiBon: number | null; }; +export type PelangganPage = { + rows: Pelanggan[]; + hasNext: boolean; +}; + +const PER_PAGE = 25; + // kdCustomer dibuat sistem, nilaiBon diisi transaksi BON; keduanya bukan isian form. export type PelangganInput = Omit; @@ -116,11 +123,14 @@ export async function deletePelanggan(kdCustomer: string): Promise { return res.affectedRows > 0; } -export async function listPelanggan(search?: string): Promise { +export async function listPelanggan( + search?: string, + page = 1, +): Promise { const q = search?.trim(); const pattern = q ? `%${escapeLike(q)}%` : ""; - return amigoDb + const rows = await amigoDb .select({ kdCustomer: pelanggan.kd_customer, nama: pelanggan.nama, @@ -145,8 +155,16 @@ export async function listPelanggan(search?: string): Promise { ) : undefined, ) - .orderBy(asc(pelanggan.nama)) - .limit(100); + // kd_customer sebagai pengurut kedua: nama bisa sama, tanpa itu urutan + // antar-query tidak stabil dan baris bisa dobel/hilang di batas halaman. + .orderBy(asc(pelanggan.nama), asc(pelanggan.kd_customer)) + .limit(PER_PAGE + 1) + .offset((page - 1) * PER_PAGE); + + return { + rows: rows.slice(0, PER_PAGE), + hasNext: rows.length > PER_PAGE, + }; } // Kode pelanggan = "A8" + 6 digit urut, pola yang sama dengan aplikasi lama