From c64cfea2a929bf5373ca4d36a691857354db4f2e Mon Sep 17 00:00:00 2001 From: ftrianaa Date: Mon, 28 Apr 2025 13:15:25 +0700 Subject: [PATCH] fix & feature: invoice --- middleware.ts | 26 ++ src/app/(admin)/all-registration/page.tsx | 271 ++++++++++++++++++- src/app/(admin)/invoice/page.tsx | 7 + src/app/(user)/history/page.tsx | 86 +++++- src/app/(user)/invoice-user/page.tsx | 7 + src/app/(user)/transactions/page.tsx | 13 +- src/app/api/action/route.ts | 26 ++ src/components/auth/SignInForm.tsx | 23 +- src/components/card/TransactionCard.tsx | 2 +- src/components/invoice/invoice.tsx | 10 +- src/database/controllers/actionController.ts | 15 + src/database/models/actionModel.ts | 11 + src/database/models/bookingModel.ts | 49 ++++ src/layout/AppSidebar.tsx | 35 +-- tsconfig.json | 2 +- 15 files changed, 524 insertions(+), 59 deletions(-) create mode 100644 src/app/(admin)/invoice/page.tsx create mode 100644 src/app/(user)/invoice-user/page.tsx create mode 100644 src/app/api/action/route.ts create mode 100644 src/database/controllers/actionController.ts create mode 100644 src/database/models/actionModel.ts diff --git a/middleware.ts b/middleware.ts index e69de29..61f1277 100644 --- a/middleware.ts +++ b/middleware.ts @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export function middleware(req: NextRequest) { + const role = req.cookies.get('role')?.value; + + const url = req.nextUrl.clone(); + + if (url.pathname === '/booking' || url.pathname === '/all-registration') { + if (role === 'admin' && url.pathname !== '/all-registration') { + url.pathname = '/all-registration'; + return NextResponse.redirect(url); + } + if (role === 'user' && url.pathname !== '/booking') { + url.pathname = '/booking'; + return NextResponse.redirect(url); + } + } + + return NextResponse.next(); +} + +// Optional: config where middleware runs +export const config = { + matcher: ['/booking', '/all-registration'], +}; diff --git a/src/app/(admin)/all-registration/page.tsx b/src/app/(admin)/all-registration/page.tsx index 7b6d6e7..25da070 100644 --- a/src/app/(admin)/all-registration/page.tsx +++ b/src/app/(admin)/all-registration/page.tsx @@ -13,6 +13,14 @@ import Image from "next/image"; import Badge from "@/components/ui/badge/Badge"; import axios from "axios"; import dayjs from "dayjs"; +import { ChevronDownIcon, PencilIcon } from "@/icons"; +import { Modal } from "@/components/ui/modal"; +import { useModal } from "@/hooks/useModal"; +import Button from "@/components/ui/button/Button"; +import Label from "@/components/form/Label"; +import Input from "@/components/form/input/InputField"; +import Select from "@/components/form/Select"; +import { useRouter } from "next/navigation"; interface Item { @@ -32,18 +40,113 @@ interface Item { export default function BasicTableOne() { - const [registration, setRegistration] = useState([]) - - const getAllRegistration = async () =>{ - const result = await axios.get("/api/registration") + const router = useRouter() - setRegistration(result.data.data) + const { isOpen, openModal, closeModal } = useModal(); + + const [registration, setRegistration] = useState([]) + const [dataModal, setDataModal] = useState({}) + const [action, setAction] = useState([]) + const [actionOption, setActionOption] = useState([]) + const [invoiceItems, setInvoiceItems] = useState([{ description: "", quantity: 1, price: 0 }]); + + const addItem = () => { + setInvoiceItems([...invoiceItems, { description: "", quantity: 1, price: 0 }]); + }; + + + const getAllRegistration = async () => { + const resultRegistration = await axios.get("/api/registration") + const resultAction = await axios.get("/api/action") + + const selectAction = resultAction.data.data.map((item: any) => { + return { + label: item.NamaTindakan, + value: item.IdTindakan, + price: item.TarifTindakan, // <- Tambahkan harga di sini + }; + }); + + + setAction(resultAction.data.data) + setActionOption(selectAction) + setRegistration(resultRegistration.data.data) } useEffect(() => { getAllRegistration() }, []) + const handleOpenModal = (data: any) => { + openModal() + setDataModal(data) + } + + const handleDeleteRow = (index: number) => { + const newItems = [...invoiceItems]; + newItems.splice(index, 1); + setInvoiceItems(newItems); + }; + + + const handleItemChange = (index, field, value) => { + console.log({ index, field, value }) + setInvoiceItems((prevItems) => { + const updatedItems = [...prevItems]; + const currentItem = { ...updatedItems[index] }; + + if (field === "description") { + currentItem.description = value; // value ini object {label, value, price} + + const prices = action.find((item: any) => { + console.log(item, "p"); + return item.IdTindakan === Number(value); + }); + + + // console.log(prices, "prices", action,value) + // Simpan harga dasar action + currentItem.actionPrice = prices.TarifTindakan || 0; + + // Hitung price awal berdasarkan quantity + currentItem.price = (currentItem.quantity || 1) * currentItem.actionPrice; + + } else if (field === "quantity") { + currentItem.quantity = value; + + // Hitung ulang price kalau ada actionPrice + if (currentItem.actionPrice) { + currentItem.price = value * currentItem.actionPrice; + } + } + + updatedItems[index] = currentItem; + return updatedItems; + }); + }; + + const submitToTrTransaksi = async () => { + + } + + const handleSubmit = async () =>{ + // invoiceItems.forEach(item => { + // const newData = { + // IdRegistrasi: Number(dataModal.IdRegistrasi), + // IdTindakan: Number(item.IdTindakan), // misalnya ini IdTindakan, sesuaikan kalau beda + // JmlTindakan: Number(item.quantity), + // Price: Number(item.price), + // IdPegawai: Number(dataModal.IdPegawai) + // }; + + // // Submit satu-satu, contoh kalau pakai API: + // submitToTrTransaksi(newData); + // }); + // console.log(invoiceItems, "invoiceItems", newData) + router.push("/invoice") + } + + return (
@@ -62,13 +165,13 @@ export default function BasicTableOne() { isHeader className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400" > - Nama + Nama - Ruang Pelayanan + Ruang Pelayanan - {`${dayjs(item.TanggalRegistrasi).format('DD MMMM YYYY')} ${item.JamKonsul}`} + {`${dayjs(item.TanggalRegistrasi).format('DD MMMM YYYY')} ${item.JamKonsul}`} - + - {item.action} + ))} @@ -155,6 +260,152 @@ export default function BasicTableOne() {
+ +
+
+
+ Tambah Transaksi +
+

+ Tambah transaksi baru dan buat invoice +

+
+
+
+
+ +

+ {dataModal.IdRegistrasi} +

+ {/* */} +
+
+
+ +

+ {dataModal.NamaPasien} +

+ +
+ +
+ +

+ {dataModal.NamaAsuransi} / {dataModal.NomorKartuAsuransi} +

+
+ + {/* Invoice Items */} +
+
+ + +
+ +
+ {invoiceItems.map((item, index) => ( +
+
+ handleItemChange(index, "quantity", Number(e.target.value))} + /> +
+ {/* Price */} +

+ Rp {item.price.toLocaleString()} +

+ + {/* Delete Button with Trash Icon */} + +
+
+ + + ))} +
+
+
+ Total: + + Rp {invoiceItems.reduce((acc, item) => acc + (item.price), 0).toLocaleString()} + +
+
+
+
+
+
+
+ + +
+
+
); } diff --git a/src/app/(admin)/invoice/page.tsx b/src/app/(admin)/invoice/page.tsx new file mode 100644 index 0000000..f832e7c --- /dev/null +++ b/src/app/(admin)/invoice/page.tsx @@ -0,0 +1,7 @@ +import InvoicePage from "@/components/invoice/invoice"; + +export default function BasicTableOne() { + return( + + ) +} \ No newline at end of file diff --git a/src/app/(user)/history/page.tsx b/src/app/(user)/history/page.tsx index 22da0cf..a319438 100644 --- a/src/app/(user)/history/page.tsx +++ b/src/app/(user)/history/page.tsx @@ -2,14 +2,96 @@ import React, { useEffect, useState } from "react"; import ComponentCard from "@/components/common/ComponentCard"; +import axios from "axios"; +import dayjs from "dayjs"; + + +const history = [ + { + eventName: 'Konsultasi dengan Pak Budi', + dateTime: '2025-04-28T10:00:00.000Z', + status: 'Confirmed', + notes: 'Discussing project progress', + }, + { + eventName: 'Konsultasi dengan Ibu Siti', + dateTime: '2025-04-28T14:00:00.000Z', + status: 'Pending', + notes: 'Discussing business strategy', + }, + { + eventName: 'Review Project', + dateTime: '2025-04-29T09:00:00.000Z', + status: 'Confirmed', + notes: 'Reviewing the latest designs', + }, + { + eventName: 'Konsultasi Pajak', + dateTime: '2025-04-29T13:00:00.000Z', + status: 'Canceled', + notes: 'Need to reschedule', + }, +]; export default function HistoryPage() { + const [bookings, setBookings] = useState([]); + + const getHistory = async () => { + const result = await axios.get("/api/registration") + + setBookings(result.data.data) + } + + useEffect(() => { + getHistory() + }, []) + + console.log(bookings) return ( -
- +
+ {bookings.map((booking, index) => { + // Membuat timestamp dari TanggalRegistrasi dan JamKonsul + const bookingTimestamp = dayjs(`${booking.TanggalRegistrasi} ${booking.JamKonsul}`, 'DD MMMM YYYY HH:mm'); + + // Membandingkan dengan waktu sekarang + const isConfirmed = bookingTimestamp.isBefore(dayjs()); // Jika bookingTimestamp lebih kecil dari waktu sekarang, berarti sudah lewat + + // Menentukan status + booking.status = isConfirmed ? 'Confirmed' : 'Pending'; + return ( + +
+
+

{booking.NamaRuangPelayanan} dengan {booking.NamaPegawai}

+

+ {`${dayjs(booking.TanggalRegistrasi).format('DD MMMM YYYY')} ${booking.JamKonsul}`} +

+

{booking.NamaAsuransi}: {booking.NomorKartuAsuransi}

+
+ + {booking.status} + +
+ ) + })}
); diff --git a/src/app/(user)/invoice-user/page.tsx b/src/app/(user)/invoice-user/page.tsx new file mode 100644 index 0000000..f832e7c --- /dev/null +++ b/src/app/(user)/invoice-user/page.tsx @@ -0,0 +1,7 @@ +import InvoicePage from "@/components/invoice/invoice"; + +export default function BasicTableOne() { + return( + + ) +} \ No newline at end of file diff --git a/src/app/(user)/transactions/page.tsx b/src/app/(user)/transactions/page.tsx index d7fda6f..0204f49 100644 --- a/src/app/(user)/transactions/page.tsx +++ b/src/app/(user)/transactions/page.tsx @@ -4,8 +4,10 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; import TransactionCard from "@/components/card/TransactionCard"; import dayjs from "dayjs"; +import { useRouter } from "next/navigation"; export default function TransactionPage() { + const router = useRouter() const [transaction, setTransaction] = useState([]) const getDataTransaction = async () =>{ @@ -18,7 +20,10 @@ export default function TransactionPage() { } } - console.log(transaction,"pp") + const handleCardClick = () =>{ + router.push('/invoice-user') + } + useEffect(() => { getDataTransaction() }, []) @@ -28,6 +33,11 @@ export default function TransactionPage() {
{transaction.map((item: any, index) => { return( +
handleCardClick(item.IdRegistrasi)} // Tindakan klik untuk menuju halaman invoice + className="cursor-pointer" // Menambahkan pointer cursor agar terlihat klikabel + > +
) })}
diff --git a/src/app/api/action/route.ts b/src/app/api/action/route.ts new file mode 100644 index 0000000..98c2081 --- /dev/null +++ b/src/app/api/action/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; // Menggunakan NextResponse +import { handleGetAction } from '@/database/controllers/actionController'; + +// Fungsi untuk menangani permintaan GET +export async function GET() { + try { + // Panggil controller untuk mendapatkan data asuransi + const response = await handleGetAction(); + + // Mengembalikan response dengan status sukses menggunakan NextResponse + return NextResponse.json({ + status: 'SUCCESS', + code: 200, + data: response, // Data hasil query + }); + } catch (error) { + console.error('Error fetching insurance:', error); + + // Jika terjadi error, kembalikan status gagal dengan error message + return NextResponse.json({ + status: 'FAILURE', + code: 500, + message: 'Internal Server Error', + }); + } +} diff --git a/src/components/auth/SignInForm.tsx b/src/components/auth/SignInForm.tsx index db27988..8b860ec 100644 --- a/src/components/auth/SignInForm.tsx +++ b/src/components/auth/SignInForm.tsx @@ -5,7 +5,7 @@ import Label from "@/components/form/Label"; import Button from "@/components/ui/button/Button"; import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons"; import Link from "next/link"; -import React, { useState } from "react"; +import React, { MouseEvent, useState } from "react"; import Cookies from "js-cookie"; import { useRouter } from "next/navigation"; @@ -18,20 +18,23 @@ export default function SignInForm() { const [showPassword, setShowPassword] = useState(false); const [isChecked, setIsChecked] = useState(false); - const handleSignIn = async () => { - // Simulasi login & ambil role dari backend - const role = email === 'admin@example.com' ? 'admin' : 'user' + const handleSignIn = async (e: MouseEvent) => { + e.preventDefault(); // ← tambahin ini + console.log("first"); - // Simpan role ke cookie - // Cookies.set('role', role) + const role = email === 'admin@example.com' ? 'admin' : 'user'; + + Cookies.set('role', role); - // Redirect ke dashboard if (role === 'admin') { - return router.push('/') + router.push('/all-registration'); + } else { + router.push('/booking'); } - return router.push('/booking') + console.log("role"); } + return (
@@ -151,7 +154,7 @@ export default function SignInForm() {
-
diff --git a/src/components/card/TransactionCard.tsx b/src/components/card/TransactionCard.tsx index 7de59aa..04b7b41 100644 --- a/src/components/card/TransactionCard.tsx +++ b/src/components/card/TransactionCard.tsx @@ -16,7 +16,7 @@ export default function TransactionCard({ totalAmount, status, }: TransactionCardProps) { - console.log( dayjs(date).add(7, 'hour').format("DD MMMM YYYY HH:mm") ,"pp") + return (
diff --git a/src/components/invoice/invoice.tsx b/src/components/invoice/invoice.tsx index 5fac7b0..00300b8 100644 --- a/src/components/invoice/invoice.tsx +++ b/src/components/invoice/invoice.tsx @@ -7,13 +7,13 @@ export default function InvoicePage() { invoiceNumber: "INV-2025-001", invoiceDate: "2025-04-27", dueDate: "2025-05-10", - customerName: "Darine Nadeva", + customerName: "Pasien A", items: [ - { description: "Website Development", quantity: 1, price: 5000000 }, - { description: "Maintenance Support", quantity: 2, price: 750000 }, + { description: "Infus", quantity: 1, price: 200000 }, + { description: "Rawat Inap", quantity: 2, price: 750000 }, ], - companyName: "Lifetime Design", - companyAddress: "Jl. Sudirman No. 99, Jakarta", + companyName: "Rumah Sakit Ibu dan Anak Harapan Kita", + companyAddress: "Jl. Letjen S. Parman No.Kav. 87, Slipi", }; const totalAmount = invoiceData.items.reduce( diff --git a/src/database/controllers/actionController.ts b/src/database/controllers/actionController.ts new file mode 100644 index 0000000..699a96b --- /dev/null +++ b/src/database/controllers/actionController.ts @@ -0,0 +1,15 @@ +import { getAllAction } from '../models/actionModel'; + +// Fungsi untuk mengambil data asuransi +export async function handleGetAction() { + try { + // Ambil data asuransi dari model + const actions = await getAllAction(); + + // Mengembalikan data asuransi + return actions; + } catch (error) { + console.error('Error fetching insurance:', error); + throw new Error('Database query failed'); + } +} diff --git a/src/database/models/actionModel.ts b/src/database/models/actionModel.ts new file mode 100644 index 0000000..3e7142d --- /dev/null +++ b/src/database/models/actionModel.ts @@ -0,0 +1,11 @@ +import { queryDb } from '../lib/db'; +export async function getAllAction() { + try { + const res = await queryDb(`SELECT * FROM "MsTindakan"`); + + return res; + } catch (error) { + console.error("Database query error:", error); + throw new Error('Database query failed'); + } +} diff --git a/src/database/models/bookingModel.ts b/src/database/models/bookingModel.ts index 9b846df..a040dae 100644 --- a/src/database/models/bookingModel.ts +++ b/src/database/models/bookingModel.ts @@ -43,3 +43,52 @@ export async function postRegister(payload: RegisterPayload) { throw new Error('Database insert failed'); } } + + +interface RegistrasiData { + NomorKartuAsuransi: number; + MRPasien: number; + IdAsuransi: number; + IdRuangPelayanan: number; + TanggalKonsul: string; + JamKonsul: string; + TanggalRegistrasi: string; + NamaPasien: string; + NamaAsuransi: string; + NamaPegawai: string; + NamaRuangPelayanan: string; + IdRegistrasi: number; +} + +// Model untuk mengambil data Registrasi +export async function getRegistrasiData(): Promise { + try { + const result = await queryDb(` + SELECT + tr."NomorKartuAsuransi", + tr."MRPasien", + tr."IdAsuransi", + tr."IdRuangPelayanan", + tr."TanggalKonsul", + tr."JamKonsul", + tr."TanggalRegistrasi", + mp."NamaPasien", + ma."NamaAsuransi", + mpgw."NamaPegawai", + mrp."NamaRuangPelayanan", + tr."IdRegistrasi" + FROM "TrRegistrasi" tr + JOIN "MsPasien" mp ON tr."MRPasien" = mp."MRPasien" + JOIN "MsAsuransi" ma ON tr."IdAsuransi" = ma."IdAsuransi" + JOIN "MsPegawai" mpgw ON tr."IdPegawai" = mpgw."IdPegawai" + JOIN "MsRuangPelayanan" mrp ON tr."IdRuangPelayanan" = mrp."IdRuangPelayanan" + WHERE tr."MRPasien" = mp."MRPasien" + ORDER BY tr."IdRegistrasi" ASC; + `); + + return result; // Mengembalikan data hasil query + } catch (error) { + console.error("Error fetching registrasi data:", error); + throw new Error('Database query failed'); + } +} \ No newline at end of file diff --git a/src/layout/AppSidebar.tsx b/src/layout/AppSidebar.tsx index e484cda..1733e59 100644 --- a/src/layout/AppSidebar.tsx +++ b/src/layout/AppSidebar.tsx @@ -40,14 +40,6 @@ const navItems: NavItem[] = [ ]; const othersItems: NavItem[] = [ - { - icon: , - name: "Charts", - subItems: [ - { name: "Line Chart", path: "/line-chart", pro: false }, - { name: "Bar Chart", path: "/bar-chart", pro: false }, - ], - }, ]; const AppSidebar: React.FC = () => { @@ -56,7 +48,7 @@ const AppSidebar: React.FC = () => { const renderMenuItems = ( navItems: NavItem[], - menuType: "main" | "others" + menuType: "main" ) => (
    {navItems.map((nav, index) => ( @@ -181,7 +173,7 @@ const AppSidebar: React.FC = () => { ); const [openSubmenu, setOpenSubmenu] = useState<{ - type: "main" | "others"; + type: "main"; index: number; } | null>(null); const [subMenuHeight, setSubMenuHeight] = useState>( @@ -195,14 +187,14 @@ const AppSidebar: React.FC = () => { useEffect(() => { // Check if the current path matches any submenu item let submenuMatched = false; - ["main", "others"].forEach((menuType) => { + ["main"].forEach((menuType) => { const items = menuType === "main" ? navItems : othersItems; items.forEach((nav, index) => { if (nav.subItems) { nav.subItems.forEach((subItem) => { if (isActive(subItem.path)) { setOpenSubmenu({ - type: menuType as "main" | "others", + type: menuType as "main", index, }); submenuMatched = true; @@ -231,7 +223,7 @@ const AppSidebar: React.FC = () => { } }, [openSubmenu]); - const handleSubmenuToggle = (index: number, menuType: "main" | "others") => { + const handleSubmenuToggle = (index: number, menuType: "main") => { setOpenSubmenu((prevOpenSubmenu) => { if ( prevOpenSubmenu && @@ -312,22 +304,7 @@ const AppSidebar: React.FC = () => { {renderMenuItems(navItems, "main")}
-
-

- {isExpanded || isHovered || isMobileOpen ? ( - "Others" - ) : ( - - )} -

- {renderMenuItems(othersItems, "others")} -
+
diff --git a/tsconfig.json b/tsconfig.json index c133409..438dfbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/types/app/(user)/transactions/page.tsx"], "exclude": ["node_modules"] }