fix & feature: invoice

This commit is contained in:
ftrianaa 2025-04-28 13:15:25 +07:00
parent 24d9bf4d12
commit c64cfea2a9
15 changed files with 524 additions and 59 deletions

View File

@ -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'],
};

View File

@ -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 (
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-white/[0.05] dark:bg-white/[0.03]">
<div className="max-w-full overflow-x-auto">
@ -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
</TableCell>
<TableCell
isHeader
className="px-5 py-3 font-medium text-gray-500 text-start text-theme-xs dark:text-gray-400"
>
Ruang Pelayanan
Ruang Pelayanan
</TableCell>
<TableCell
isHeader
@ -129,9 +232,9 @@ export default function BasicTableOne() {
{item.NamaPegawai}
</TableCell>
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
{`${dayjs(item.TanggalRegistrasi).format('DD MMMM YYYY')} ${item.JamKonsul}`}
{`${dayjs(item.TanggalRegistrasi).format('DD MMMM YYYY')} ${item.JamKonsul}`}
</TableCell>
<TableCell className="px-4 py-3 text-gray-500 text-start text-theme-sm dark:text-gray-400">
<Badge
size="sm"
@ -147,7 +250,9 @@ export default function BasicTableOne() {
</Badge>
</TableCell>
<TableCell className="px-4 py-3 text-gray-500 text-theme-sm dark:text-gray-400">
{item.action}
<Button size="sm" variant="outline" onClick={() => handleOpenModal(item)}>
<PencilIcon />
</Button>
</TableCell>
</TableRow>
))}
@ -155,6 +260,152 @@ export default function BasicTableOne() {
</Table>
</div>
</div>
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
Tambah Transaksi
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
Tambah transaksi baru dan buat invoice
</p>
</div>
<div className="mt-8">
<div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
ID Rgeistrasi
</label>
<p className="text-sm text-gray-500 dark:text-gray-400">
{dataModal.IdRegistrasi}
</p>
{/* <input
id="event-title"
type="text"
className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
/> */}
</div>
</div>
<div className="mt-6">
<label className="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-400">
Nama
</label>
<p className="text-sm text-gray-500 dark:text-gray-400">
{dataModal.NamaPasien}
</p>
</div>
<div className="mt-6">
<label className="block mb-1.5 text-sm font-medium text-gray-700 dark:text-gray-400">
Asuransi / No Asuransi
</label>
<p className="text-sm text-gray-500 dark:text-gray-400">
{dataModal.NamaAsuransi} / {dataModal.NomorKartuAsuransi}
</p>
</div>
{/* Invoice Items */}
<div className="space-y-4 mt-8">
<div className="flex justify-between items-center">
<Label>Invoice Items</Label>
<button
onClick={addItem}
className="text-sm font-medium px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
+ Add Item
</button>
</div>
<div className="space-y-4">
{invoiceItems.map((item, index) => (
<div key={index} className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative">
<Select
options={actionOption}
placeholder="Select an option"
onChange={(e) => handleItemChange(index, "description", e)}
className="dark:bg-dark-900"
/>
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
<ChevronDownIcon />
</span>
</div>
<Input
type="number"
placeholder="Quantity"
value={item.quantity}
onChange={(e) => handleItemChange(index, "quantity", Number(e.target.value))}
/>
<div className="flex items-center justify-between space-x-4">
{/* Price */}
<p className="flex items-center h-11 rounded-md px-4 text-right">
Rp {item.price.toLocaleString()}
</p>
{/* Delete Button with Trash Icon */}
<button
type="button"
onClick={() => handleDeleteRow(index)}
className="flex items-center justify-center h-8 w-8 rounded-full bg-red-500 text-white hover:bg-red-600 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="h-5 w-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
))}
<div className="mt-6 flex justify-end">
<div className="w-full md:w-1/3">
<div className="flex items-center justify-between border-t border-gray-300 pt-4 dark:border-gray-700">
<span className="font-semibold">Total:</span>
<span className="font-bold">
Rp {invoiceItems.reduce((acc, item) => acc + (item.price), 0).toLocaleString()}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<button
onClick={closeModal}
type="button"
className="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto"
>
Close
</button>
<button
onClick={handleSubmit}
type="button"
className="btn btn-success btn-update-event flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto"
>
Submit
</button>
</div>
</div>
</Modal>
</div>
);
}

View File

@ -0,0 +1,7 @@
import InvoicePage from "@/components/invoice/invoice";
export default function BasicTableOne() {
return(
<InvoicePage/>
)
}

View File

@ -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 (
<ComponentCard title="History Jadwal">
<div className="space-y-6">
<div className="space-y-4">
{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 (
<div
key={index}
className={`p-4 rounded-lg border-2 ${booking.status === 'Confirmed'
? 'border-green-500 bg-green-50'
: booking.status === 'Pending'
? 'border-yellow-500 bg-yellow-50'
: 'border-red-500 bg-red-50'
} flex justify-between items-center`}
>
<div className="space-y-2">
<h3 className="text-lg font-semibold">{booking.NamaRuangPelayanan} dengan {booking.NamaPegawai}</h3>
<p className="text-sm text-gray-600">
{`${dayjs(booking.TanggalRegistrasi).format('DD MMMM YYYY')} ${booking.JamKonsul}`}
</p>
<p className="text-sm text-gray-500">{booking.NamaAsuransi}: {booking.NomorKartuAsuransi}</p>
</div>
<span
className={`px-3 py-1 text-white rounded-full ${booking.status === 'Confirmed'
? 'bg-green-500'
: booking.status === 'Pending'
? 'bg-yellow-500'
: 'bg-red-500'
}`}
>
{booking.status}
</span>
</div>
)
})}
</div>
</ComponentCard>
);

View File

@ -0,0 +1,7 @@
import InvoicePage from "@/components/invoice/invoice";
export default function BasicTableOne() {
return(
<InvoicePage/>
)
}

View File

@ -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() {
<div className="space-y-6">
{transaction.map((item: any, index) => {
return(
<div
key={index}
onClick={() => handleCardClick(item.IdRegistrasi)} // Tindakan klik untuk menuju halaman invoice
className="cursor-pointer" // Menambahkan pointer cursor agar terlihat klikabel
>
<TransactionCard
key={index}
customerName={item.NamaPasien}
@ -36,6 +46,7 @@ export default function TransactionPage() {
totalAmount={0}
status={"Unpaid"}
/>
</div>
)
})}
</div>

View File

@ -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',
});
}
}

View File

@ -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<HTMLButtonElement>) => {
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 (
<div className="flex flex-col flex-1 lg:w-1/2 w-full">
<div className="w-full max-w-md sm:pt-10 mx-auto mb-5">
@ -151,7 +154,7 @@ export default function SignInForm() {
</Link>
</div>
<div>
<Button className="w-full" size="sm" onClick={handleSignIn}>
<Button className="w-full" size="sm" onClick={(e) => handleSignIn(e)}>
Sign in
</Button>
</div>

View File

@ -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 (
<div className="bg-white p-6 rounded-lg shadow-md flex flex-col gap-2 hover:shadow-lg transition">
<div className="flex justify-between items-center">

View File

@ -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(

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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<RegistrasiData[]> {
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');
}
}

View File

@ -40,14 +40,6 @@ const navItems: NavItem[] = [
];
const othersItems: NavItem[] = [
{
icon: <PieChartIcon />,
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"
) => (
<ul className="flex flex-col gap-4">
{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<Record<string, number>>(
@ -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")}
</div>
<div className="">
<h2
className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${
!isExpanded && !isHovered
? "lg:justify-center"
: "justify-start"
}`}
>
{isExpanded || isHovered || isMobileOpen ? (
"Others"
) : (
<HorizontaLDots />
)}
</h2>
{renderMenuItems(othersItems, "others")}
</div>
</div>
</nav>
</div>

View File

@ -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"]
}