Compare commits

...

2 Commits

@ -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 (
<>
<div className="mt-4 overflow-hidden rounded-lg border">
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-sm">
<thead>
<tr className="border-b bg-muted/50 text-left text-xs tracking-wide text-muted-foreground uppercase">
<th className="px-3 py-2.5 font-medium">Kode</th>
<th className="px-3 py-2.5 font-medium">Nama</th>
<th className="px-3 py-2.5 font-medium">Alias</th>
<th className="px-3 py-2.5 font-medium">Alamat</th>
<th className="px-3 py-2.5 font-medium">Ponsel</th>
<th className="px-3 py-2.5 text-right font-medium">Bon</th>
<th className="px-3 py-2.5 text-right font-medium">Aksi</th>
</tr>
</thead>
<tbody className="divide-y">
{rows.map((p) => (
<tr
key={p.kdCustomer}
className="transition-colors hover:bg-muted/40"
>
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap text-muted-foreground">
{p.kdCustomer}
</td>
<td className="px-3 py-2 font-medium">{p.nama}</td>
<td className="px-3 py-2 whitespace-nowrap text-muted-foreground">
{p.alias}
</td>
<td className="px-3 py-2 text-muted-foreground">
{p.alamat}
</td>
<td className="px-3 py-2 whitespace-nowrap text-muted-foreground">
{p.ponsel ?? "-"}
</td>
<td className="px-3 py-2 text-right tabular-nums">
{p.nilaiBon !== null && p.nilaiBon > 0 ? p.nilaiBon : "-"}
</td>
<td className="px-3 py-2">
<div className="flex justify-end gap-1">
<PelangganForm
pelanggan={p}
trigger={
<Button
variant="ghost"
size="icon-sm"
title="Edit pelanggan"
>
<PencilIcon />
<span className="sr-only">Edit</span>
</Button>
}
/>
<HapusPelangganButton
kdCustomer={p.kdCustomer}
nama={p.nama}
/>
</div>
</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td
colSpan={7}
className="px-3 py-10 text-center text-muted-foreground"
>
{page > 1 ? "Halaman ini kosong" : "Tidak ada pelanggan"}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<Pagination
page={page}
hasNext={hasNext}
hrefPage={(p) => 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";
}

@ -1,17 +1,13 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { DaftarPelanggan } from "./_components/daftar-pelanggan";
import { listPelanggan } from "@/data/pelanggan";
import { HapusPelangganButton } from "./_components/hapus-pelanggan-button";
import { PelangganForm } from "./_components/pelanggan-form";
import { TambahPelangganButton } from "./_components/tambah-pelanggan-button"; import { TambahPelangganButton } from "./_components/tambah-pelanggan-button";
import { CariPelanggan } from "./_components/cari-pelanggan"; import { CariPelanggan } from "./_components/cari-pelanggan";
export default function PelangganPage({ export default function PelangganPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ search?: string }>; searchParams: Promise<{ search?: string; page?: string }>;
}) { }) {
return ( return (
<div className="p-4"> <div className="p-4">
@ -30,61 +26,3 @@ export default function PelangganPage({
</div> </div>
); );
} }
async function DaftarPelanggan({
searchParams,
}: {
searchParams: Promise<{ search?: string }>;
}) {
const { search } = await searchParams;
const daftar = await listPelanggan(search);
return (
<table className="mt-4 w-full text-sm">
<thead>
<tr className="border-b text-left">
<th className="py-2 pr-4">Kode</th>
<th className="py-2 pr-4">Nama</th>
<th className="py-2 pr-4">Alias</th>
<th className="py-2 pr-4">Alamat</th>
<th className="py-2 pr-4">Ponsel</th>
<th className="py-2 pr-4">Bon</th>
<th className="py-2 pr-4">Aksi</th>
</tr>
</thead>
<tbody>
{daftar.map((p) => (
<tr key={p.kdCustomer} className="border-b">
<td className="py-2 pr-4">{p.kdCustomer}</td>
<td className="py-2 pr-4">{p.nama}</td>
<td className="py-2 pr-4">{p.alias}</td>
<td className="py-2 pr-4">{p.alamat}</td>
<td className="py-2 pr-4">{p.ponsel ?? "-"}</td>
<td className="py-2 pr-4">
{p.nilaiBon !== null && p.nilaiBon > 0 ? p.nilaiBon : "-"}
</td>
<td className="flex gap-1 py-2 pr-4">
<PelangganForm
pelanggan={p}
trigger={
<Button variant="ghost" size="icon-sm" title="Edit pelanggan">
<PencilIcon />
<span className="sr-only">Edit</span>
</Button>
}
/>
<HapusPelangganButton kdCustomer={p.kdCustomer} nama={p.nama} />
</td>
</tr>
))}
{daftar.length === 0 && (
<tr>
<td colSpan={7} className="py-4 text-center text-muted-foreground">
Tidak ada pelanggan
</td>
</tr>
)}
</tbody>
</table>
);
}

@ -1,6 +1,5 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export default function NotFoundPage() { export default function NotFoundPage() {

@ -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 (
<div className="mt-4 flex items-center justify-between text-sm">
<span className="text-muted-foreground">Halaman {page}</span>
<div className="flex gap-2">
<TombolHalaman href={page > 1 ? hrefPage(page - 1) : null}>
Sebelumnya
</TombolHalaman>
<TombolHalaman href={hasNext ? hrefPage(page + 1) : null}>
Berikutnya
</TombolHalaman>
</div>
</div>
);
}
function TombolHalaman({
href,
children,
}: {
href: string | null;
children: ReactNode;
}) {
const className = buttonVariants({ variant: "outline", size: "sm" });
if (!href) {
return (
<span className={cn(className, "pointer-events-none opacity-50")}>
{children}
</span>
);
}
return (
<Link href={href} className={className}>
{children}
</Link>
);
}

@ -20,6 +20,13 @@ export type Pelanggan = {
nilaiBon: number | null; 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. // kdCustomer dibuat sistem, nilaiBon diisi transaksi BON; keduanya bukan isian form.
export type PelangganInput = Omit<Pelanggan, "kdCustomer" | "nilaiBon">; export type PelangganInput = Omit<Pelanggan, "kdCustomer" | "nilaiBon">;
@ -116,11 +123,14 @@ export async function deletePelanggan(kdCustomer: string): Promise<boolean> {
return res.affectedRows > 0; return res.affectedRows > 0;
} }
export async function listPelanggan(search?: string): Promise<Pelanggan[]> { export async function listPelanggan(
search?: string,
page = 1,
): Promise<PelangganPage> {
const q = search?.trim(); const q = search?.trim();
const pattern = q ? `%${escapeLike(q)}%` : ""; const pattern = q ? `%${escapeLike(q)}%` : "";
return amigoDb const rows = await amigoDb
.select({ .select({
kdCustomer: pelanggan.kd_customer, kdCustomer: pelanggan.kd_customer,
nama: pelanggan.nama, nama: pelanggan.nama,
@ -145,8 +155,16 @@ export async function listPelanggan(search?: string): Promise<Pelanggan[]> {
) )
: undefined, : undefined,
) )
.orderBy(asc(pelanggan.nama)) // kd_customer sebagai pengurut kedua: nama bisa sama, tanpa itu urutan
.limit(100); // 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 // Kode pelanggan = "A8" + 6 digit urut, pola yang sama dengan aplikasi lama

Loading…
Cancel
Save