data-table
✅ 1. Masalah Edit dan Delete Belum Berfungsi
Di getUserColumns
, kamu cuma nge-console.log
saat tombol edit/delete diklik:
const columns = getUserColumns(
(r) => console.log("Update", r),
(r) => console.log("Delete", r)
);
🔧 Solusi:
Tambahkan state dan handler di UserTable
untuk memicu edit/delete yang sesungguhnya:
const [editingUser, setEditingUser] = useState<User | null>(null);
const handleUpdateClick = (user: User) => {
setEditingUser(user);
setUSername(user.username);
setPassword(user.password);
setNo(user.no_hp);
setStatus(user.status);
setAlamat(user.alamat);
setEmail(user.email.toString());
};
const handleDeleteClick = async (user: User) => {
const token = localStorage.getItem("adminToken");
try {
const res = await fetch(`http://localhost:8000/api/user/${user.username}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error("Gagal hapus user");
setData((prev) => prev.filter((u) => u.username !== user.username));
} catch (err) {
console.log("Delete Error", err);
}
};
Ganti pemanggilan fungsi ini di bawah:
const columns = getUserColumns(handleUpdateClick, handleDeleteClick);
✅ 2. Tombol Submit Tidak Memperbarui User Jika Mode Edit
Sekarang handleSubmit
hanya menambah user baru. Kalau kamu sedang edit, perlu membedakan logic-nya.
🔧 Solusi:
Modifikasi handleSubmit
:
const handleSubmit = async () => {
const formData = new FormData();
formData.append("username", username);
formData.append("password", password);
formData.append("no_hp", no_hp);
formData.append("status", status);
formData.append("alamat", alamat);
formData.append("email", email);
const token = localStorage.getItem("adminToken");
try {
const url = editingUser
? `http://127.0.0.1:8000/api/user/${editingUser.username}`
: "http://127.0.0.1:8000/api/user/";
const method = editingUser ? "PUT" : "POST";
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (!response.ok) {
throw new Error("Gagal simpan data");
}
const newUser = await response.json();
if (editingUser) {
// Update data di list
setData((prev) =>
prev.map((u) => (u.username === editingUser.username ? newUser : u))
);
} else {
// Tambahkan user baru
setData((prev) => [...prev, newUser]);
}
// Reset form
setUSername("");
setPassword("");
setStatus("");
setAlamat("");
setNo("");
setEmail("");
setEditingUser(null);
} catch (err) {
console.log("Submit Error", err);
}
};
✅ 3. Tampilkan Nama Dialog Berubah Saat Edit
Ganti bagian ini:
<DialogTitle>Tambahkan User</DialogTitle>
Menjadi:
<DialogTitle>{editingUser ? "Edit User" : "Tambahkan User"}</DialogTitle>
✅ 4. Bug Minor: Tipe Email
Kamu pakai email: number;
di User
, padahal email itu string
.
export type User = {
username: string;
password: string;
email: string; // ini sebelumnya number
no_hp: string;
status: string;
alamat: string;
};
Kalau kamu butuh bantuin struktur update endpoint dari backend biar cocok (PUT dengan FormData), tinggal bilang aja ya.
Mau sekalian ku bantuin styling atau pop-up konfirmasi delete juga?
1. Menu
"use client"
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { PlusIcon } from "lucide-react";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
interface Menuitem {
id_menu: number;
nama_menu: string;
harga: number;
menu_image: string;
id_kategori:number;
}
const Features05Page = () => {
const [menus, setMenu] = useState<Menuitem[]>([]);
const [namaMenu, setNamaMenu] = useState("");
const [harga, setHarga] = useState("");
const [imageFile, setImageFile] = useState<File | null>(null);
const [kategori,setKategori]=useState(1)
const handleSubmit = async () => {
const token = localStorage.getItem("adminToken");
const formData = new FormData();
formData.append("nama_menu", namaMenu);
formData.append("harga", harga);
formData.append("id_kategori", kategori.toString()); // pastikan string
if (imageFile) {
formData.append("menu_image", imageFile);
}
try {
const response = await fetch("http://127.0.0.1:8000/api/menu/", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
// jangan tambahkan content-type, biarkan browser mengatur sendiri
},
body: formData,
});
if (!response.ok) {
throw new Error("Gagal menambahkan menu");
}
const data = await response.json();
setMenu((prev) => [...prev, data]); // update UI langsung
setNamaMenu("");
setHarga("");
setKategori(1)
setImageFile(null);
} catch (error) {
console.error("Error:", error);
}
};
useEffect(() => {
const token = localStorage.getItem("adminToken");
fetch("http://127.0.0.1:8000/api/menu/", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => response.json())
.then((data: Menuitem[]) => setMenu(data))
.catch((error) => console.log("error", error));
}, []);
return (
<div className="max-w-screen-lg w-full py-10 px-6">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-4">
Daftar Menu
</h1>
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="w-[140px] font-normal bg-gray-100"
>
<PlusIcon className="mr-2" /> Tambah Menu
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tambah Menu Baru</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="nama" className="text-right">
Nama
</Label>
<Input
id="nama"
value={namaMenu}
onChange={(e) => setNamaMenu(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="harga" className="text-right">
Harga
</Label>
<Input
id="harga"
value={harga}
onChange={(e) => setHarga(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="kategori" className="text-right">
kategori
</Label>
<Input
id="kategori"
value={kategori}
onChange={(e) => setKategori(parseInt(e.target.value))}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="gambar" className="text-right">
Gambar
</Label>
<Input
id="gambar"
type="file"
accept="image/*"
onChange={(e) => setImageFile(e.target.files?.[0] || null)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleSubmit}>Simpan</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="mt-8 w-full mx-auto grid md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-8">
{menus.map((menu) => (
<Card
key={menu.id_menu}
className="flex flex-col border rounded-xl overflow-hidden shadow-none"
>
<CardHeader>
<h4 className="!mt-3 text-xl font-semibold tracking-tight">
{menu.nama_menu}
</h4>
<p className="mt-1 text-muted-foreground text-[17px]">
{menu.harga}
</p>
</CardHeader>
<CardContent className="mt-auto px-0 pb-0">
<div>
<img
src={menu.menu_image}
alt="Error"
className="bg-muted h-40 ml-6 rounded-tl-xl"
/>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default Features05Page;
- Meja
import { useEffect, useState } from "react";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { Input } from "../ui/input";
import { Label } from "@/components/ui/label";
import { PlusIcon } from "lucide-react";
const features = [
{
title: "Identify Opportunities",
description: "Find untapped areas to explore effortlessly.",
},
{
title: "Build Authority",
description: "Craft content that resonates and inspires trust.",
},
{
title: "Instant Insights",
description: "Get actionable insights instantly at a glance.",
},
];
interface MejaItem {
id_meja: number;
no_meja: number;
kapasitas: number;
image_meja: string;
}
const Features02Page = () => {
const [mejas, setMeja] = useState<MejaItem[]>([]);
const [no_meja, setNomeja] = useState("");
const [id_meja, setIDMeja] = useState("");
const [kapasitas, setKapasitas] = useState("");
const [imageMeja, setImageMeja] = useState<File | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
const token = localStorage.getItem("adminToken");
try {
fetch("http://127.0.0.1:8000/api/meja/", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => response.json())
.then((data: MejaItem[]) => setMeja(data))
.catch((error) => console.log("error ", error));
} catch (error) {
console.log("error ", error);
}
}, []);
const handleSubmit = async () => {
const token = localStorage.getItem("adminToken");
const newForm = new FormData();
newForm.append("no_meja", no_meja);
newForm.append("kapasitas", kapasitas);
if (imageMeja) {
newForm.append("image_meja", imageMeja);
}
try {
const response = await fetch("http://127.0.0.1:8000/api/meja/", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: newForm,
});
if (!response.ok) {
throw new Error("Gagal menambahkan meja");
}
const data = await response.json();
setMeja((prev) => [...prev, data]);
setNomeja("");
setKapasitas("");
setImageMeja(null);
setOpen(false);
} catch (error) {
console.log(error);
}
};
return (
<div className="min-h-screen flex justify-center py-12">
<div className="w-full">
<h2 className="text-4xl sm:text-5xl font-bold tracking-tight mb-14 text-center">
Daftar Meja
</h2>
<div className="ml-[315px]">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="w-[140px] font-normal bg-gray-100"
>
<PlusIcon className="mr-2" /> Tambah Menu
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tambah Menu Baru</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="no_meja" className="text-right">
No Meja
</Label>
<Input
id="no_meja"
value={no_meja}
onChange={(e) => setNomeja(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="kapasitas" className="text-right">
Kapasitas
</Label>
<Input
id="kapasitas"
value={kapasitas}
onChange={(e) => setKapasitas(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="gambar" className="text-right">
Gambar
</Label>
<Input
id="gambar"
type="file"
accept="image/*"
onChange={(e) => setImageMeja(e.target.files?.[0] || null)}
className="col-span-3"
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleSubmit}>Simpan</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="mt-10 grid sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-12 max-w-md sm:max-w-screen-md lg:max-w-screen-lg w-full mx-auto px-6">
{mejas.map((meja) => (
<div key={meja.id_meja} className="flex flex-col text-start">
<div>
<img
src={meja.image_meja}
alt="Image Not Found"
className="mb-5 sm:mb-6 w-full aspect-[3/3] bg-muted rounded-xl"
/>
</div>
<span className="text-2xl font-semibold tracking-tight">
Nomer Meja : {meja.no_meja}
</span>
<p className="mt-2 max-w-[25ch] text-muted-foreground text-[17px]">
Kapasitas : {meja.kapasitas}
</p>
</div>
))}
</div>
</div>
</div>
);
};
export default Features02Page;
- Reservasi
// page
"use client"
import React from "react"
import { ReservationTable } from "./data-table"
export default function ReservationPage() {
return (
<div className="p-4">
<h1 className="text-xl font-bold mb-4">Reservation List</h1>
<ReservationTable />
</div>
)
}
// column.tsx
import { ColumnDef } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ArrowUpDown, Edit, Trash2 } from "lucide-react"
import { Reservation } from "./data-table"
export function getReservationColumns(
handleUpdateClick: (reservation: Reservation) => void,
handleDeleteClick: (reservation: Reservation) => void
): ColumnDef<Reservation>[] {
return [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
header: "ID",
cell: ({ row }) => <div>{row.index + 1}</div>,
},
{
accessorKey: "nama_customer",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Customer Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => <div>{row.getValue("nama_customer")}</div>,
},
{
accessorKey: "tanggal_reservasi",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => {
const date = new Date(row.getValue("tanggal_reservasi"))
return <div>{date.toLocaleDateString("en-GB")}</div>
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string
const statusClass = {
booked: "text-blue-600",
confirmed: "text-green-600",
pending: "text-amber-600",
cancelled: "text-red-600",
}[status.toLowerCase()] || ""
return <div className={`capitalize font-medium ${statusClass}`}>{status}</div>
},
},
{
accessorKey: "id_meja",
header: "Table No.",
cell: ({ row }) => <div>Table {row.getValue("id_meja")}</div>,
},
{
id: "actions",
enableHiding: false,
header: "Actions",
cell: ({ row }) => {
const reservation = row.original
return (
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleUpdateClick(reservation)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => handleDeleteClick(reservation)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)
},
},
]
}
// data table
"use client";
import React, { use, useState } from "react";
import {
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getReservationColumns } from "./column";
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
import {
Dialog,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DialogFooter, DialogHeader } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { log } from "console";
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
import { SelectTrigger, SelectValue } from "@radix-ui/react-select";
export type Reservation = {
id_reservasi: number;
nama_customer: string;
tanggal_reservasi: string;
status: string;
id_meja: number;
id_admin: number;
};
export function ReservationTable() {
const [data, setData] = React.useState<Reservation[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [id_admin, setIdAdmin] = useState("");
const [nama_customer, setNama] = useState("");
const [tanggal_reservasi, setTanggal] = useState("");
const [status, setStatus] = useState("");
const [id_meja, setMeja] = useState("");
const fetchReservations = async () => {
try {
setLoading(true);
const token = localStorage.getItem("adminToken");
const response = await fetch("http://localhost:8000/api/reservasi/", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) throw new Error("Failed to fetch reservation data");
const result = await response.json();
setData(Array.isArray(result) ? result : [result]);
} catch (err) {
setError("Failed to load reservations. Please try again later.");
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchReservations();
}, []);
const handleSubmit = async () => {
const formData = new FormData();
formData.append("id_admin", id_admin);
formData.append("nama_customer", nama_customer);
formData.append("tanggal_reservasi", tanggal_reservasi);
formData.append("status", status);
formData.append("id_meja", id_meja);
const token = localStorage.getItem("adminToken");
try {
const response = await fetch("http://127.0.0.1:8000/api/reservasi/", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (!response.ok) {
throw new Error("Gagal menambahkan reservasi");
}
const data = await response.json();
setData((prev) => [...prev, data]);
setIdAdmin("");
setNama("");
setStatus("");
setTanggal("");
setMeja("");
} catch (err) {
console.log("Error ", err);
}
};
const columns = getReservationColumns(
(r) => console.log("Update", r),
(r) => console.log("Delete", r)
);
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
if (loading)
return (
<div className="flex justify-center items-center h-48">
Loading reservation data...
</div>
);
if (error)
return (
<div className="text-red-500 flex justify-center items-center h-48">
{error}
</div>
);
return (
<div className="w-full">
<div className="flex items-center py-4">
<Input
placeholder="Filter by customer name..."
value={
(table.getColumn("nama_customer")?.getFilterValue() as string) ?? ""
}
onChange={(e) =>
table.getColumn("nama_customer")?.setFilterValue(e.target.value)
}
className="max-w-sm"
/>
<Dialog>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
className="w-[160px] ml-2 font-normal bg-gray-100"
>
<PlusIcon className="mr-2" /> Tambah Reservasi
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tambahkan Reservasi</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="id_admin" className="text-right">
Id Admin
</Label>
<Input
id="id_admin"
value={id_admin}
onChange={(e) => setIdAdmin(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="meja" className="text-right">
Id Meja
</Label>
<Input
id="meja"
value={id_meja}
onChange={(e) => setMeja(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="nama" className="text-right">
Nama
</Label>
<Input
id="nama"
value={nama_customer}
onChange={(e) => setNama(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="status" className="text-right">
Status
</Label>
<Select value={status} onValueChange={setStatus}>
<SelectTrigger
id="status"
className="col-span-3 border border-input bg-background rounded-md px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
>
<SelectValue placeholder="Pilih status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Pending">Pending</SelectItem>
<SelectItem value="Booked">Booked</SelectItem>
<SelectItem value="Success">Success</SelectItem>
<SelectItem value="Cancelled">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tanggal" className="text-right">
Tanggal
</Label>
<Input
id="tanggal_reservasi"
value={tanggal_reservasi}
onChange={(e) => setTanggal(e.target.value)}
className="col-span-3"
type="date"
/>
</div>
<DialogFooter>
<Button onClick={handleSubmit}>Simpan</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}
- Report
'use client'
import ProductCard from "@/components/reportcard"
import { DataTableDemo } from "./table"
export default function Report(){
return(
<div>
<DataTableDemo/>
<div>
<ProductCard/>
</div>
</div>
)
}
// table.tsx
"use client"
import React, { useEffect, useState } from "react"
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
// 1. Tipe data
export type Payment = {
id: string
nama_customer: string
grand_total: number
status_pembayaran: "Pending" | "Processing" | "Success" | "Failed"
}
// 2. Kolom tetap sama
export const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "status_pembayaran",
header: "Status",
cell: ({ row }) => (
<div className="capitalize">{row.getValue("status_pembayaran")}</div>
),
},
{
accessorKey: "nama_customer",
header: ({ column }) => (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Nama Customer
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
cell: ({ row }) => <div className="lowercase">{row.getValue("nama_customer")}</div>,
},
{
accessorKey: "grand_total",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("grand_total"))
return <div className="text-right font-medium">{amount}</div>
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const payment = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
// 3. Komponen utama
export function DataTableDemo() {
const [data, setData] = useState<Payment[]>([])
const [loading, setLoading] = useState(true)
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
const [rowSelection, setRowSelection] = useState({})
// 4. Fetch API saat mount
useEffect(() => {
const fetchData = async () => {
const token = localStorage.getItem("adminToken")
try {
const res = await fetch("http://127.0.0.1:8000/api/report/",{
headers:{
Authorization : `Bearer ${token}`
}
})
const json = await res.json()
setData(json)
} catch (error) {
console.error("Failed to fetch data", error)
} finally {
setLoading(false)
}
}
fetchData()
}, [])
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
})
return (
<div className="w-full">
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn("nama_customer")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("nama_customer")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
Loading...
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
)
}
Top comments (0)