diff --git a/app/(dashboard)/_components/nav-items.ts b/app/(dashboard)/_components/nav-items.ts
index dd195ac..28ca6d0 100644
--- a/app/(dashboard)/_components/nav-items.ts
+++ b/app/(dashboard)/_components/nav-items.ts
@@ -19,6 +19,9 @@ export const navItems: NavItem[] = [
{
title: "Master Data",
icon: DatabaseIcon,
- items: [{ title: "Satuan Barang", href: "/master/satuan-barang" }, { title: "Data Pelanggan", href: "/master/pelanggan" }],
+ items: [
+ { title: "Satuan Barang", href: "/master/satuan-barang" },
+ { title: "Data Pelanggan", href: "/master/pelanggan" },
+ ],
},
];
diff --git a/app/(dashboard)/master/pelanggan/_components/hapus-pelanggan-button.tsx b/app/(dashboard)/master/pelanggan/_components/hapus-pelanggan-button.tsx
index a8b89e7..b611980 100644
--- a/app/(dashboard)/master/pelanggan/_components/hapus-pelanggan-button.tsx
+++ b/app/(dashboard)/master/pelanggan/_components/hapus-pelanggan-button.tsx
@@ -1,18 +1,20 @@
"use client";
-import { useActionState, useEffect, useState } from "react";
+import { useActionState, useState } from "react";
import { Loader2Icon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { hapusPelanggan, type HapusPelangganState } from "../action";
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { hapusPelanggan, type HapusPelangganState } from "../actions";
type HapusPelangganButtonProps = {
kdCustomer: string;
@@ -26,57 +28,53 @@ export function HapusPelangganButton({
nama,
}: HapusPelangganButtonProps) {
const [open, setOpen] = useState(false);
- const [state, formAction, pending] = useActionState(hapusPelanggan, initial);
-
- useEffect(() => {
- if (state.ok) setOpen(false);
- }, [state.ok]);
+ const [state, formAction, pending] = useActionState(
+ async (prevState: HapusPelangganState, formData: FormData) => {
+ const result = await hapusPelanggan(prevState, formData);
+ if (result.ok) setOpen(false);
+ return result;
+ },
+ initial,
+ );
return (
-
-
+
-
-
- Hapus {nama}?
-
+
+
+ Hapus {nama}?
+
Data pelanggan {kdCustomer} akan dihapus permanen.
-
-
+
+
-
-
-
+
+
);
}
diff --git a/app/(dashboard)/master/pelanggan/_components/pelanggan-form.tsx b/app/(dashboard)/master/pelanggan/_components/pelanggan-form.tsx
index ee93add..2e26d82 100644
--- a/app/(dashboard)/master/pelanggan/_components/pelanggan-form.tsx
+++ b/app/(dashboard)/master/pelanggan/_components/pelanggan-form.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useActionState, useEffect, useState } from "react";
+import { useActionState, useState } from "react";
import { Loader2Icon } from "lucide-react";
import {
@@ -15,8 +15,8 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
-import { simpanPelanggan, type SimpanPelangganState } from "../action";
-import { Pelanggan } from "@/data/pelanggan";
+import { simpanPelanggan, type SimpanPelangganState } from "../actions";
+import type { Pelanggan } from "@/data/pelanggan";
type PelangganFormProps = {
pelanggan?: Pelanggan;
@@ -27,11 +27,16 @@ const initial: SimpanPelangganState = { ok: false };
export function PelangganForm({ pelanggan, trigger }: PelangganFormProps) {
const [open, setOpen] = useState(false);
- const [state, formAction, pending] = useActionState(simpanPelanggan, initial);
-
- useEffect(() => {
- if (state.ok) setOpen(false);
- }, [state.ok]);
+ // Sheet ditutup di sini, bukan di useEffect: effect dengan deps [state.ok] tidak
+ // jalan lagi saat simpan sukses kedua (true -> true), jadi sheet tetap terbuka.
+ const [state, formAction, pending] = useActionState(
+ async (prevState: SimpanPelangganState, formData: FormData) => {
+ const result = await simpanPelanggan(prevState, formData);
+ if (result.ok) setOpen(false);
+ return result;
+ },
+ initial,
+ );
return (
@@ -60,99 +65,80 @@ export function PelangganForm({ pelanggan, trigger }: PelangganFormProps) {
/>
)}
-
- Nama *
-
-
-
-
-
- Alias
-
-
-
-
- No. KTP
-
-
-
-
- Alamat *
-
-
-
-
- Wilayah
-
-
-
-
- Telepon
-
-
-
-
- Ponsel
-
-
-
-
- Email
-
-
-
-
- Jenis Customer
-
-
-
-
- Upline
-
-
-
-
- BTS Bon
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {state.error && (
+ {state.error}
+ )}
);
}
+
+type FieldName = keyof NonNullable;
+
+type FormInputProps = Omit<
+ React.ComponentProps<"input">,
+ "name" | "defaultValue"
+> & {
+ name: FieldName;
+ label: string;
+ state: SimpanPelangganState;
+ defaultValue?: string | number | null;
+};
+
+// Isian dari server (state.values) didahulukan supaya ketikan user tidak hilang
+// saat validasi gagal; kalau belum ada, pakai data pelanggan yang sedang diedit.
+function FormInput({
+ name,
+ label,
+ state,
+ defaultValue,
+ ...props
+}: FormInputProps) {
+ const errors = state.fieldErrors?.[name];
+
+ return (
+
+ {label}
+
+ ({ message }))} />
+
+ );
+}
diff --git a/app/(dashboard)/master/pelanggan/_components/tambah-pelanggan-button.tsx b/app/(dashboard)/master/pelanggan/_components/tambah-pelanggan-button.tsx
index f1bc007..fe2d9ae 100644
--- a/app/(dashboard)/master/pelanggan/_components/tambah-pelanggan-button.tsx
+++ b/app/(dashboard)/master/pelanggan/_components/tambah-pelanggan-button.tsx
@@ -4,23 +4,15 @@ import { PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { PelangganForm } from "./pelanggan-form";
-import React from "react";
-interface Props {
- trigger?: React.ReactElement;
-}
-export function TambahPelangganButton({ trigger }: Props) {
+export function TambahPelangganButton() {
return (
- Tambah
-
-
- )
+
}
/>
);
diff --git a/app/(dashboard)/master/pelanggan/action.ts b/app/(dashboard)/master/pelanggan/actions.ts
similarity index 61%
rename from app/(dashboard)/master/pelanggan/action.ts
rename to app/(dashboard)/master/pelanggan/actions.ts
index be06706..ae111b5 100644
--- a/app/(dashboard)/master/pelanggan/action.ts
+++ b/app/(dashboard)/master/pelanggan/actions.ts
@@ -7,10 +7,18 @@ import {
deletePelanggan,
insertPelanggan,
updatePelanggan,
- PelangganInput,
+ type PelangganInput,
} from "@/data/pelanggan";
-export type SimpanPelangganState = { ok: boolean; error?: string; kdCustomer?: string };
+type PelangganField = keyof typeof pelangganSchema.shape;
+export type SimpanPelangganState = {
+ ok: boolean;
+ error?: string;
+ fieldErrors?: Partial>;
+ values?: Partial>;
+ kdCustomer?: string;
+};
+
export type HapusPelangganState = { ok: boolean; error?: string };
const pelangganSchema = z.object({
@@ -31,25 +39,24 @@ function emptyStringToNull(value: string): string | null {
return value === "" ? null : value;
}
-export async function simpanPelanggan(_prevState: SimpanPelangganState, formData: FormData): Promise {
- await getCurrentUser();
+export async function simpanPelanggan(
+ _prevState: SimpanPelangganState,
+ formData: FormData,
+): Promise {
+ const user = await getCurrentUser();
- const parsed = pelangganSchema.safeParse({
- nama: formData.get("nama"),
- alias: formData.get("alias"),
- ktp: formData.get("ktp"),
- alamat: formData.get("alamat"),
- wilayah: formData.get("wilayah"),
- telp: formData.get("telp"),
- ponsel: formData.get("ponsel"),
- email: formData.get("email"),
- jenisCustomer: formData.get("jenisCustomer"),
- upline: formData.get("upline"),
- btsBon: formData.get("btsBon"),
- })
+ const fields = Object.keys(pelangganSchema.shape) as PelangganField[];
+ const values = Object.fromEntries(
+ fields.map((field) => [field, String(formData.get(field) ?? "")]),
+ ) as Record;
+ const parsed = pelangganSchema.safeParse(values);
if (!parsed.success) {
- return { ok: false, error: parsed.error.issues[0].message }
+ return {
+ ok: false,
+ fieldErrors: z.flattenError(parsed.error).fieldErrors,
+ values,
+ };
}
const input: PelangganInput = {
@@ -60,8 +67,7 @@ export async function simpanPelanggan(_prevState: SimpanPelangganState, formData
email: emptyStringToNull(parsed.data.email),
jenisCustomer: emptyStringToNull(parsed.data.jenisCustomer),
upline: emptyStringToNull(parsed.data.upline),
- }
-
+ };
const kdCustomer = String(formData.get("kdCustomer") ?? "").trim();
try {
@@ -73,13 +79,21 @@ export async function simpanPelanggan(_prevState: SimpanPelangganState, formData
const updated = await updatePelanggan(kdCustomer, input);
if (!updated) {
- return { ok: false, error: "Pelanggan tidak ditemukan" };
+ return { ok: false, error: "Pelanggan tidak ditemukan", values };
}
revalidatePath("/master/pelanggan");
return { ok: true, kdCustomer };
} catch (error) {
- console.error("simpan pelanggan gagal", error);
- return { ok: false, error: "Gagal menyimpan pelanggan" };
+ console.error(
+ "[simpanPelanggan] gagal",
+ { userId: user.id, kdCustomer: kdCustomer || "(baru)", nama: input.nama },
+ error,
+ );
+ return {
+ ok: false,
+ error: "Gagal menyimpan pelanggan, coba simpan ulang",
+ values,
+ };
}
}
@@ -87,7 +101,7 @@ export async function hapusPelanggan(
_prevState: HapusPelangganState,
formData: FormData,
): Promise {
- await getCurrentUser();
+ const user = await getCurrentUser();
const kdCustomer = String(formData.get("kdCustomer") ?? "").trim();
if (kdCustomer === "") {
@@ -102,7 +116,14 @@ export async function hapusPelanggan(
revalidatePath("/master/pelanggan");
return { ok: true };
} catch (error) {
- console.error("hapus pelanggan gagal", error);
- return { ok: false, error: "Gagal menghapus pelanggan" };
+ console.error(
+ "[hapusPelanggan] gagal",
+ { userId: user.id, kdCustomer },
+ error,
+ );
+ return {
+ ok: false,
+ error: "Gagal menghapus pelanggan, coba hapus ulang",
+ };
}
-}
\ No newline at end of file
+}
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..4d88edf
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,187 @@
+"use client"
+
+import * as React from "react"
+import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
+import { cn } from "cn"
+
+import { Button } from "@/components/ui/button"
+
+function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
+ return
+}
+
+function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: AlertDialogPrimitive.Backdrop.Props) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ size = "default",
+ ...props
+}: AlertDialogPrimitive.Popup.Props & {
+ size?: "default" | "sm"
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogMedia({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ variant = "outline",
+ size = "default",
+ ...props
+}: AlertDialogPrimitive.Close.Props &
+ Pick, "variant" | "size">) {
+ return (
+ }
+ {...props}
+ />
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+}
diff --git a/data/pelanggan.ts b/data/pelanggan.ts
index 9422c46..b88ca37 100644
--- a/data/pelanggan.ts
+++ b/data/pelanggan.ts
@@ -20,43 +20,8 @@ export type Pelanggan = {
nilaiBon: number | null;
};
-export type PelangganInput = {
- nama: string;
- alias: string;
- ktp: string;
- alamat: string;
- wilayah: string | null;
- telp: string | null;
- ponsel: string | null;
- email: string | null;
- jenisCustomer: string | null;
- upline: string | null;
- btsBon: number | null;
-};
-
-export async function getPelanggan(
- kdCustomer: string,
-): Promise {
- const [row] = await amigoDb
- .select({
- nama: pelanggan.nama,
- alias: pelanggan.alias,
- ktp: pelanggan.ktp,
- alamat: pelanggan.alamat,
- wilayah: pelanggan.wilayah,
- telp: pelanggan.telp,
- ponsel: pelanggan.ponsel,
- email: pelanggan.email,
- jenisCustomer: pelanggan.jenis_customer,
- upline: pelanggan.upline,
- btsBon: pelanggan.bts_bon,
- })
- .from(pelanggan)
- .where(eq(pelanggan.kd_customer, kdCustomer))
- .limit(1);
-
- return row ?? null;
-}
+// kdCustomer dibuat sistem, nilaiBon diisi transaksi BON; keduanya bukan isian form.
+export type PelangganInput = Omit;
// Tanggal hari ini di zona toko (WIB) sebagai `yyyy-mm-dd`; toISOString() memakai UTC
// sehingga jam 00:00-07:00 tersimpan sebagai tanggal kemarin.
@@ -175,9 +140,9 @@ export async function listPelanggan(search?: string): Promise {
.where(
q
? or(
- like(pelanggan.nama, pattern),
- like(pelanggan.kd_customer, pattern),
- )
+ like(pelanggan.nama, pattern),
+ like(pelanggan.kd_customer, pattern),
+ )
: undefined,
)
.orderBy(asc(pelanggan.nama))