Add customer master page with list, add, edit, delete, and search
parent
6abd3051c5
commit
b0bf1d5bf7
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function CariPelanggan() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [nilai, setNilai] = useState(searchParams.get("search") ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
const current = searchParams.get("search") ?? "";
|
||||
if (nilai === current) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const url = nilai
|
||||
? `/master/pelanggan?search=${encodeURIComponent(nilai)}`
|
||||
: "/master/pelanggan";
|
||||
router.replace(url);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [nilai, searchParams, router]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
className="pl-8"
|
||||
placeholder="Cari pelanggan..."
|
||||
value={nilai}
|
||||
onChange={(e) => setNilai(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, 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";
|
||||
|
||||
type HapusPelangganButtonProps = {
|
||||
kdCustomer: string;
|
||||
nama: string;
|
||||
};
|
||||
|
||||
const initial: HapusPelangganState = { ok: false };
|
||||
|
||||
export function HapusPelangganButton({
|
||||
kdCustomer,
|
||||
nama,
|
||||
}: HapusPelangganButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [state, formAction, pending] = useActionState(hapusPelanggan, initial);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) setOpen(false);
|
||||
}, [state.ok]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setOpen(true)}
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
title="Hapus pelanggan"
|
||||
>
|
||||
<Trash2Icon />
|
||||
<span className="sr-only">Hapus</span>
|
||||
</Button>
|
||||
|
||||
<SheetContent
|
||||
side="top"
|
||||
className="data-[side=top]:inset-x-4 data-[side=top]:top-24 data-[side=top]:mx-auto data-[side=top]:w-full data-[side=top]:max-w-sm data-[side=top]:rounded-lg data-[side=top]:border data-[side=top]:border-t"
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Hapus {nama}?</SheetTitle>
|
||||
<SheetDescription>
|
||||
Data pelanggan {kdCustomer} akan dihapus permanen.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<form action={formAction} className="p-4 pt-0">
|
||||
<input type="hidden" name="kdCustomer" value={kdCustomer} />
|
||||
{state.error && (
|
||||
<p className="mb-2 text-sm text-destructive">{state.error}</p>
|
||||
)}
|
||||
<SheetFooter className="flex-row justify-end px-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={pending}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" variant="destructive" disabled={pending}>
|
||||
{pending && <Loader2Icon className="size-4 animate-spin" />}
|
||||
Hapus
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect, useState } from "react";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
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";
|
||||
|
||||
type PelangganFormProps = {
|
||||
pelanggan?: Pelanggan;
|
||||
trigger: React.ReactElement;
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger render={trigger} />
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{pelanggan ? `Edit ${pelanggan.nama}` : "Tambah Pelanggan"}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
{pelanggan
|
||||
? `Kode: ${pelanggan.kdCustomer}`
|
||||
: "Isi detail pelanggan baru"}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<form
|
||||
action={formAction}
|
||||
className="flex flex-col gap-4 overflow-y-auto p-4"
|
||||
>
|
||||
{pelanggan && (
|
||||
<input
|
||||
type="hidden"
|
||||
name="kdCustomer"
|
||||
value={pelanggan.kdCustomer}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="nama">Nama *</FieldLabel>
|
||||
<Input
|
||||
id="nama"
|
||||
name="nama"
|
||||
defaultValue={pelanggan?.nama}
|
||||
required
|
||||
/>
|
||||
<FieldError
|
||||
errors={state.error ? [{ message: state.error }] : []}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="alias">Alias</FieldLabel>
|
||||
<Input id="alias" name="alias" defaultValue={pelanggan?.alias} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ktp">No. KTP</FieldLabel>
|
||||
<Input id="ktp" name="ktp" defaultValue={pelanggan?.ktp} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="alamat">Alamat *</FieldLabel>
|
||||
<Input
|
||||
id="alamat"
|
||||
name="alamat"
|
||||
defaultValue={pelanggan?.alamat}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="wilayah">Wilayah</FieldLabel>
|
||||
<Input
|
||||
id="wilayah"
|
||||
name="wilayah"
|
||||
defaultValue={pelanggan?.wilayah ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="telp">Telepon</FieldLabel>
|
||||
<Input id="telp" name="telp" defaultValue={pelanggan?.telp ?? ""} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="ponsel">Ponsel</FieldLabel>
|
||||
<Input
|
||||
id="ponsel"
|
||||
name="ponsel"
|
||||
defaultValue={pelanggan?.ponsel ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Email</FieldLabel>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={pelanggan?.email ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="jenisCustomer">Jenis Customer</FieldLabel>
|
||||
<Input
|
||||
id="jenisCustomer"
|
||||
name="jenisCustomer"
|
||||
defaultValue={pelanggan?.jenisCustomer ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="upline">Upline</FieldLabel>
|
||||
<Input
|
||||
id="upline"
|
||||
name="upline"
|
||||
defaultValue={pelanggan?.upline ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="btsBon">BTS Bon</FieldLabel>
|
||||
<Input
|
||||
id="btsBon"
|
||||
name="btsBon"
|
||||
type="number"
|
||||
defaultValue={pelanggan?.btsBon ?? ""}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<SheetFooter className="px-0">
|
||||
<Button type="submit" disabled={pending}>
|
||||
{pending && <Loader2Icon className="size-4 animate-spin" />}
|
||||
Simpan
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
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) {
|
||||
return (
|
||||
<PelangganForm
|
||||
trigger={
|
||||
trigger ? (
|
||||
trigger
|
||||
) : (
|
||||
<Button>
|
||||
Tambah
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { getCurrentUser } from "@/data/auth";
|
||||
import {
|
||||
deletePelanggan,
|
||||
insertPelanggan,
|
||||
updatePelanggan,
|
||||
PelangganInput,
|
||||
} from "@/data/pelanggan";
|
||||
|
||||
export type SimpanPelangganState = { ok: boolean; error?: string; kdCustomer?: string };
|
||||
export type HapusPelangganState = { ok: boolean; error?: string };
|
||||
|
||||
const pelangganSchema = z.object({
|
||||
nama: z.string().trim().min(1, "Nama wajib diisi").max(100),
|
||||
alias: z.string().trim().max(11),
|
||||
ktp: z.string().trim().max(16),
|
||||
alamat: z.string().trim().min(1, "Alamat wajib diisi").max(100),
|
||||
wilayah: z.string().trim().max(200),
|
||||
telp: z.string().trim().max(15),
|
||||
ponsel: z.string().trim().max(15),
|
||||
email: z.string().trim().max(30),
|
||||
jenisCustomer: z.string().trim().max(1),
|
||||
upline: z.string().trim().max(11),
|
||||
btsBon: z.coerce.number().int().min(0).max(2_000_000_000),
|
||||
});
|
||||
|
||||
function emptyStringToNull(value: string): string | null {
|
||||
return value === "" ? null : value;
|
||||
}
|
||||
|
||||
export async function simpanPelanggan(_prevState: SimpanPelangganState, formData: FormData): Promise<SimpanPelangganState> {
|
||||
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"),
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return { ok: false, error: parsed.error.issues[0].message }
|
||||
}
|
||||
|
||||
const input: PelangganInput = {
|
||||
...parsed.data,
|
||||
wilayah: emptyStringToNull(parsed.data.wilayah),
|
||||
telp: emptyStringToNull(parsed.data.telp),
|
||||
ponsel: emptyStringToNull(parsed.data.ponsel),
|
||||
email: emptyStringToNull(parsed.data.email),
|
||||
jenisCustomer: emptyStringToNull(parsed.data.jenisCustomer),
|
||||
upline: emptyStringToNull(parsed.data.upline),
|
||||
}
|
||||
|
||||
const kdCustomer = String(formData.get("kdCustomer") ?? "").trim();
|
||||
|
||||
try {
|
||||
if (kdCustomer === "") {
|
||||
const kdBaru = await insertPelanggan(input);
|
||||
revalidatePath("/master/pelanggan");
|
||||
return { ok: true, kdCustomer: kdBaru };
|
||||
}
|
||||
|
||||
const updated = await updatePelanggan(kdCustomer, input);
|
||||
if (!updated) {
|
||||
return { ok: false, error: "Pelanggan tidak ditemukan" };
|
||||
}
|
||||
revalidatePath("/master/pelanggan");
|
||||
return { ok: true, kdCustomer };
|
||||
} catch (error) {
|
||||
console.error("simpan pelanggan gagal", error);
|
||||
return { ok: false, error: "Gagal menyimpan pelanggan" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function hapusPelanggan(
|
||||
_prevState: HapusPelangganState,
|
||||
formData: FormData,
|
||||
): Promise<HapusPelangganState> {
|
||||
await getCurrentUser();
|
||||
|
||||
const kdCustomer = String(formData.get("kdCustomer") ?? "").trim();
|
||||
if (kdCustomer === "") {
|
||||
return { ok: false, error: "Kode pelanggan tidak ada" };
|
||||
}
|
||||
|
||||
try {
|
||||
const deleted = await deletePelanggan(kdCustomer);
|
||||
if (!deleted) {
|
||||
return { ok: false, error: "Pelanggan tidak ditemukan" };
|
||||
}
|
||||
revalidatePath("/master/pelanggan");
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
console.error("hapus pelanggan gagal", error);
|
||||
return { ok: false, error: "Gagal menghapus pelanggan" };
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
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 { TambahPelangganButton } from "./_components/tambah-pelanggan-button";
|
||||
import { CariPelanggan } from "./_components/cari-pelanggan";
|
||||
|
||||
export default function PelangganPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ search?: string }>;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-lg font-semibold">Data Pelanggan</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Suspense fallback={null}>
|
||||
<CariPelanggan />
|
||||
</Suspense>
|
||||
<TambahPelangganButton />
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={<p className="mt-4 text-sm">Memuat...</p>}>
|
||||
<DaftarPelanggan searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
import "server-only";
|
||||
|
||||
import { asc, desc, like, or, eq } from "drizzle-orm";
|
||||
import { amigoDb } from "@/db/amigo";
|
||||
import { pelanggan } from "@/db/amigo/schema";
|
||||
|
||||
export type Pelanggan = {
|
||||
kdCustomer: string;
|
||||
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;
|
||||
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<PelangganInput | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
// Tanggal hari ini di zona toko (WIB) sebagai `yyyy-mm-dd`; toISOString() memakai UTC
|
||||
// sehingga jam 00:00-07:00 tersimpan sebagai tanggal kemarin.
|
||||
function todayWib(): string {
|
||||
return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Jakarta" }).format(
|
||||
new Date(),
|
||||
);
|
||||
}
|
||||
|
||||
// `\`, `%`, dan `_` adalah karakter khusus LIKE (escape default MariaDB `\`).
|
||||
function escapeLike(value: string): string {
|
||||
return value.replace(/[\\%_]/g, "\\$&");
|
||||
}
|
||||
|
||||
function isKdCustomerExist(error: unknown): boolean {
|
||||
const e = error as {
|
||||
errno?: number;
|
||||
code?: string;
|
||||
cause?: { errno?: number; code?: string };
|
||||
};
|
||||
|
||||
return (
|
||||
e?.errno === 1062 ||
|
||||
e?.code === "ER_DUP_ENTRY" ||
|
||||
e?.cause?.errno === 1062 ||
|
||||
e?.cause?.code === "ER_DUP_ENTRY"
|
||||
);
|
||||
}
|
||||
|
||||
export async function insertPelanggan(input: PelangganInput): Promise<string> {
|
||||
// MyISAM tidak punya transaksi; baca max + insert bisa balapan dengan PHP lama,
|
||||
// jadi kalau kode sudah dipakai proses lain, ulangi baca max + insert.
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const kdCustomer = await generateKodePelanggan();
|
||||
try {
|
||||
await amigoDb.insert(pelanggan).values({
|
||||
kd_customer: kdCustomer,
|
||||
alias: input.alias,
|
||||
ktp: input.ktp,
|
||||
// PHP lama tidak mengisi `member`, jadi selalu tanggal nol.
|
||||
member: "0000-00-00",
|
||||
nama: input.nama,
|
||||
alamat: input.alamat,
|
||||
wilayah: input.wilayah,
|
||||
telp: input.telp,
|
||||
ponsel: input.ponsel,
|
||||
email: input.email,
|
||||
upline: input.upline,
|
||||
jenis_customer: input.jenisCustomer,
|
||||
bts_bon: input.btsBon,
|
||||
tgl_entry: todayWib(),
|
||||
});
|
||||
|
||||
return kdCustomer;
|
||||
} catch (error) {
|
||||
if (!isKdCustomerExist(error) || attempt === 2) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Kode pelanggan baru sedang dipakai proses lain");
|
||||
}
|
||||
|
||||
export async function updatePelanggan(
|
||||
kdCustomer: string,
|
||||
input: PelangganInput,
|
||||
): Promise<boolean> {
|
||||
const [res] = await amigoDb
|
||||
.update(pelanggan)
|
||||
.set({
|
||||
alias: input.alias,
|
||||
ktp: input.ktp,
|
||||
nama: input.nama,
|
||||
alamat: input.alamat,
|
||||
wilayah: input.wilayah,
|
||||
telp: input.telp,
|
||||
ponsel: input.ponsel,
|
||||
email: input.email,
|
||||
upline: input.upline,
|
||||
jenis_customer: input.jenisCustomer,
|
||||
bts_bon: input.btsBon,
|
||||
})
|
||||
.where(eq(pelanggan.kd_customer, kdCustomer));
|
||||
|
||||
return res.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function deletePelanggan(kdCustomer: string): Promise<boolean> {
|
||||
const [res] = await amigoDb
|
||||
.delete(pelanggan)
|
||||
.where(eq(pelanggan.kd_customer, kdCustomer));
|
||||
|
||||
return res.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function listPelanggan(search?: string): Promise<Pelanggan[]> {
|
||||
const q = search?.trim();
|
||||
const pattern = q ? `%${escapeLike(q)}%` : "";
|
||||
|
||||
return amigoDb
|
||||
.select({
|
||||
kdCustomer: pelanggan.kd_customer,
|
||||
nama: pelanggan.nama,
|
||||
alias: pelanggan.alias,
|
||||
email: pelanggan.email,
|
||||
alamat: pelanggan.alamat,
|
||||
telp: pelanggan.telp,
|
||||
ponsel: pelanggan.ponsel,
|
||||
jenisCustomer: pelanggan.jenis_customer,
|
||||
btsBon: pelanggan.bts_bon,
|
||||
nilaiBon: pelanggan.nilai_bon,
|
||||
ktp: pelanggan.ktp,
|
||||
wilayah: pelanggan.wilayah,
|
||||
upline: pelanggan.upline,
|
||||
})
|
||||
.from(pelanggan)
|
||||
.where(
|
||||
q
|
||||
? or(
|
||||
like(pelanggan.nama, pattern),
|
||||
like(pelanggan.kd_customer, pattern),
|
||||
)
|
||||
: undefined,
|
||||
)
|
||||
.orderBy(asc(pelanggan.nama))
|
||||
.limit(100);
|
||||
}
|
||||
|
||||
// Kode pelanggan = "A8" + 6 digit urut, pola yang sama dengan aplikasi lama
|
||||
// (old-source/ajaxpelanggan.php ambil_kode).
|
||||
export async function generateKodePelanggan(): Promise<string> {
|
||||
const [row] = await amigoDb
|
||||
.select({ kdCustomer: pelanggan.kd_customer })
|
||||
.from(pelanggan)
|
||||
.orderBy(desc(pelanggan.kd_customer))
|
||||
.limit(1);
|
||||
|
||||
const urut = Number.parseInt(row?.kdCustomer.slice(2) ?? "0", 10) + 1;
|
||||
return `A8${String(urut).padStart(6, "0")}`;
|
||||
}
|
||||
Loading…
Reference in New Issue