DEV Community

Vytroo
Vytroo

Posted on • Edited on

Fitur Restoran NextJS

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)
);
Enter fullscreen mode Exit fullscreen mode

🔧 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

Ganti pemanggilan fungsi ini di bawah:

const columns = getUserColumns(handleUpdateClick, handleDeleteClick);
Enter fullscreen mode Exit fullscreen mode

✅ 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);
  }
};
Enter fullscreen mode Exit fullscreen mode

✅ 3. Tampilkan Nama Dialog Berubah Saat Edit

Ganti bagian ini:

<DialogTitle>Tambahkan User</DialogTitle>
Enter fullscreen mode Exit fullscreen mode

Menjadi:

<DialogTitle>{editingUser ? "Edit User" : "Tambahkan User"}</DialogTitle>
Enter fullscreen mode Exit fullscreen mode

✅ 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;
};
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode
"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;
Enter fullscreen mode Exit fullscreen mode
  1. 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;

Enter fullscreen mode Exit fullscreen mode
  1. 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>
        )
      },
    },
  ]
}

Enter fullscreen mode Exit fullscreen mode
// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode
  1. 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)