feat: laravel app instalasi sim-rs harapan kita
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Vincent Chandra Trie Novan 2025-04-27 20:02:24 +07:00
parent 24b67f7810
commit 3dc3727505
3 changed files with 329 additions and 231 deletions

View File

@ -78,8 +78,8 @@ class TransactionController extends Controller
{
$patients = Patient::select(['id', 'name'])->get();
$registrations = Registration::with(['patient:id,name'])
->whereDoesntHave('transaction')
->where('status', '!=', 'cancelled')
->where('payment_status', '!=', 'paid')
->select(['id', 'registration_number', 'patient_id'])
->get()
->map(function ($registration) {
@ -90,7 +90,7 @@ class TransactionController extends Controller
];
});
$insurances = Insurance::select(['id', 'name'])->get();
$procedures = Procedure::select(['id', 'code', 'name'])->get();
$procedures = Procedure::select(['id', 'code', 'name', 'base_price', 'tax_percentage', 'is_taxable'])->get();
$employees = Employee::with('user:id,name')
->whereHas('user')
->get()
@ -131,52 +131,49 @@ class TransactionController extends Controller
*/
public function store(Request $request)
{
// Validasi input dari request
// Validate the main transaction data
$validated = $request->validate([
'registration_id' => 'required|exists:t_registration,id',
'patient_id' => 'required|exists:m_patient,id',
'insurance_id' => 'nullable|exists:m_insurance,id',
'procedure_id' => 'required|exists:m_procedure,id',
'employee_id' => 'nullable|exists:m_employee,id',
'service_name' => 'required|string|max:100',
'transaction_datetime' => 'required|date',
'payment_datetime' => 'nullable|date',
'due_date' => 'nullable|date',
'procedure_id' => 'required|exists:m_procedure,id', // ID prosedur harus ada dan valid
'quantity' => 'required|integer|min:1', // Jumlah harus ada, berupa integer dan minimal 1
'subtotal' => 'required|numeric|min:0',
'tax_amount' => 'required|numeric|min:0',
'discount_amount' => 'nullable|numeric|min:0',
'grand_total' => 'required|numeric|min:0',
'paid_amount' => 'required|numeric|min:0',
'insurance_covered_amount' => 'nullable|numeric|min:0',
'payment_method' => 'required|in:cash,debit_card,credit_card,transfer,insurance,other',
'payment_reference' => 'nullable|string|max:100',
'status' => 'required|in:pending,paid,partially_paid,cancelled,refunded',
'notes' => 'nullable|string',
]);
// Mulai transaksi database
return DB::transaction(function () use ($validated) {
// Generate nomor invoice
// Start database transaction
return DB::transaction(function () use ($validated, $request) {
// Generate invoice number
$invoiceNumber = Transaction::generateInvoiceNumber();
// Ambil harga prosedur
// Get procedure details
$procedure = Procedure::findOrFail($validated['procedure_id']);
$unitPrice = $procedure->base_price;
// Hitung subtotal
$subtotal = $unitPrice * $validated['quantity'];
// Set default values for optional fields
$discountAmount = $validated['discount_amount'] ?? 0;
$insuranceCoveredAmount = $validated['insurance_covered_amount'] ?? 0;
$patientResponsibility = $validated['grand_total'] - $insuranceCoveredAmount;
// Hitung pajak dan diskon jika ada
$taxAmount = $subtotal * ($procedure->tax_percentage / 100);
$discountAmount = 0; // Anda bisa menambahkan logika diskon jika diperlukan
// Hitung grand total
$grandTotal = $subtotal + $taxAmount - $discountAmount;
// Hitung tanggung jawab pasien
$patientResponsibility = $grandTotal - ($validated['insurance_id'] ? $validated['insurance_covered_amount'] : 0);
// Hitung jumlah kembalian jika pembayaran dilakukan
// Calculate change amount if payment made
$changeAmount = 0;
if ($validated['paid_amount'] > $patientResponsibility) {
$changeAmount = $validated['paid_amount'] - $patientResponsibility;
}
// Buat transaksi
// Create transaction
$transaction = Transaction::create([
'invoice_number' => $invoiceNumber,
'registration_id' => $validated['registration_id'],
@ -185,33 +182,37 @@ class TransactionController extends Controller
'cashier_id' => auth()->id(),
'transaction_datetime' => $validated['transaction_datetime'],
'payment_datetime' => $validated['payment_datetime'],
'due_date' => $validated['due_date'],
'subtotal' => $subtotal,
'tax_amount' => $taxAmount,
'due_date' => $validated['due_date'] ?? null,
'subtotal' => $validated['subtotal'],
'tax_amount' => $validated['tax_amount'],
'discount_amount' => $discountAmount,
'grand_total' => $grandTotal,
'grand_total' => $validated['grand_total'],
'paid_amount' => $validated['paid_amount'],
'change_amount' => $changeAmount,
'insurance_covered_amount' => $validated['insurance_covered_amount'] ?? 0,
'insurance_covered_amount' => $insuranceCoveredAmount,
'patient_responsibility' => $patientResponsibility,
'payment_method' => $validated['payment_method'],
'payment_reference' => $validated['payment_reference'],
'payment_reference' => $validated['payment_reference'] ?? null,
'status' => $validated['status'],
'notes' => $validated['notes'],
'notes' => $validated['notes'] ?? null,
]);
// Buat detail transaksi
// Create transaction detail
TransactionDetail::create([
'transaction_id' => $transaction->id,
'procedure_id' => $validated['procedure_id'],
'quantity' => $validated['quantity'],
'unit_price' => $unitPrice,
'performed_by' => $validated['employee_id'],
'procedure_code' => $procedure->code,
'procedure_name' => $validated['service_name'],
'quantity' => 1, // Default quantity to 1
'unit_price' => $validated['subtotal'],
'discount_amount' => $discountAmount,
'tax_amount' => $taxAmount,
'subtotal' => $subtotal,
'tax_amount' => $validated['tax_amount'],
'subtotal' => $validated['subtotal'],
'notes' => $validated['notes'] ?? null,
]);
// Update status pembayaran registrasi jika diperlukan
// Update registration payment status if needed
if ($validated['status'] === 'paid') {
$registration = Registration::findOrFail($validated['registration_id']);
$registration->update(['payment_status' => 'paid']);
@ -220,7 +221,6 @@ class TransactionController extends Controller
$registration->update(['payment_status' => 'partial']);
}
// Redirect ke halaman transaksi dengan pesan sukses
return redirect()->route('transactions.index')
->with('status', 'Transaksi berhasil dibuat');
});
@ -274,6 +274,15 @@ class TransactionController extends Controller
];
});
// Get the first detail for the form
$firstDetail = $transaction->details->first();
// Hitung ulang patient_responsibility jika diperlukan
$patientResponsibility = $transaction->patient_responsibility;
if ($transaction->payment_method === 'insurance') {
$patientResponsibility = $transaction->grand_total - $transaction->insurance_covered_amount;
}
return Inertia::render('transactions/form', [
'mode' => 'edit',
'transaction' => [
@ -282,39 +291,25 @@ class TransactionController extends Controller
'registration_id' => $transaction->registration_id,
'patient_id' => $transaction->patient_id,
'insurance_id' => $transaction->insurance_id,
'cashier_id' => $transaction->cashier_id,
'procedure_id' => $firstDetail ? $firstDetail->procedure_id : null,
'employee_id' => $firstDetail ? $firstDetail->performed_by : null,
'service_name' => $firstDetail ? $firstDetail->procedure_name : '',
'transaction_datetime' => $transaction->transaction_datetime->format('Y-m-d\TH:i'),
'payment_datetime' => $transaction->payment_datetime?->format('Y-m-d\TH:i'),
'due_date' => $transaction->due_date?->format('Y-m-d'),
'subtotal' => $transaction->subtotal,
'tax_amount' => $transaction->tax_amount,
'discount_amount' => $transaction->discount_amount,
'subtotal' => (float) $transaction->subtotal,
'tax_amount' => (float) $transaction->tax_amount,
'discount_amount' => (float) $transaction->discount_amount,
'discount_reason' => $transaction->discount_reason,
'grand_total' => $transaction->grand_total,
'paid_amount' => $transaction->paid_amount,
'change_amount' => $transaction->change_amount,
'insurance_covered_amount' => $transaction->insurance_covered_amount,
'patient_responsibility' => $transaction->patient_responsibility,
'grand_total' => (float) $transaction->grand_total,
'paid_amount' => (float) $transaction->paid_amount,
'change_amount' => (float) $transaction->change_amount,
'insurance_covered_amount' => (float) $transaction->insurance_covered_amount,
'patient_responsibility' => (float) $patientResponsibility,
'payment_method' => $transaction->payment_method,
'payment_reference' => $transaction->payment_reference,
'status' => $transaction->status,
'notes' => $transaction->notes,
'details' => $transaction->details->map(function($detail) {
return [
'id' => $detail->id,
'procedure_id' => $detail->procedure_id,
'performed_by' => $detail->performed_by,
'code' => $detail->code,
'procedure_name' => $detail->procedure_name,
'quantity' => $detail->quantity,
'unit_price' => $detail->unit_price,
'discount_amount' => $detail->discount_amount,
'tax_amount' => $detail->tax_amount,
'subtotal' => $detail->subtotal,
'performed_datetime' => $detail->performed_datetime?->format('Y-m-d\TH:i'),
'notes' => $detail->notes,
];
}),
],
'patients' => $patients,
'registrations' => $registrations,
@ -348,39 +343,40 @@ class TransactionController extends Controller
'registration_id' => 'required|exists:t_registration,id',
'patient_id' => 'required|exists:m_patient,id',
'insurance_id' => 'nullable|exists:m_insurance,id',
'procedure_id' => 'required|exists:m_procedure,id',
'employee_id' => 'nullable|exists:m_employee,id',
'service_name' => 'required|string|max:100',
'transaction_datetime' => 'required|date',
'payment_datetime' => 'nullable|date',
'due_date' => 'nullable|date',
'details' => 'required|array|min:1',
'details.*.procedure_id' => 'required|exists:m_procedure,id',
'details.*.quantity' => 'required|integer|min:1',
'details.*.performed_by' => 'nullable|exists:m_employee,id',
'details.*.notes' => 'nullable|string',
'subtotal' => 'required|numeric|min:0',
'tax_amount' => 'required|numeric|min:0',
'discount_amount' => 'nullable|numeric|min:0',
'grand_total' => 'required|numeric|min:0',
'paid_amount' => 'required|numeric|min:0',
'insurance_covered_amount' => 'nullable|numeric|min:0',
'payment_method' => 'required|in:cash,debit_card,credit_card,transfer,insurance,other',
'payment_reference' => 'nullable|string|max:100',
'status' => 'required|in:pending,paid,partially_paid,cancelled,refunded',
'notes' => 'nullable|string',
]);
return DB::transaction(function () use ($validated, $transaction) {
// Hitung total baru
$subtotal = 0;
$taxAmount = 0;
$discountAmount = 0;
// Get procedure details
$procedure = Procedure::findOrFail($validated['procedure_id']);
foreach ($validated['details'] as $detail) {
$procedure = Procedure::findOrFail($detail['procedure_id']);
$unitPrice = $procedure->base_price;
$detailSubtotal = $unitPrice * $detail['quantity'];
$detailTax = $detailSubtotal * ($procedure->tax_percentage / 100);
// Set default values for optional fields
$discountAmount = $validated['discount_amount'] ?? 0;
$insuranceCoveredAmount = $validated['insurance_covered_amount'] ?? 0;
$patientResponsibility = $validated['grand_total'] - $insuranceCoveredAmount;
$subtotal += $detailSubtotal;
$taxAmount += $detailTax;
// Calculate change amount if payment made
$changeAmount = 0;
if ($validated['paid_amount'] > $patientResponsibility) {
$changeAmount = $validated['paid_amount'] - $patientResponsibility;
}
// Hitung grand total
$grandTotal = $subtotal + $taxAmount - $discountAmount;
// Hitung tanggung jawab pasien
$patientResponsibility = $grandTotal - ($validated['insurance_id'] ? $validated['insurance_covered_amount'] : 0);
// Update transaksi
// Update transaction
$transaction->update([
'registration_id' => $validated['registration_id'],
'patient_id' => $validated['patient_id'],
@ -388,35 +384,50 @@ class TransactionController extends Controller
'transaction_datetime' => $validated['transaction_datetime'],
'payment_datetime' => $validated['payment_datetime'],
'due_date' => $validated['due_date'],
'subtotal' => $subtotal,
'tax_amount' => $taxAmount,
'subtotal' => $validated['subtotal'],
'tax_amount' => $validated['tax_amount'],
'discount_amount' => $discountAmount,
'grand_total' => $grandTotal,
'grand_total' => $validated['grand_total'],
'paid_amount' => $validated['paid_amount'],
'change_amount' => $changeAmount,
'insurance_covered_amount' => $insuranceCoveredAmount,
'patient_responsibility' => $patientResponsibility,
'payment_method' => $validated['payment_method'],
'payment_reference' => $validated['payment_reference'],
'status' => $validated['status'],
'notes' => $validated['notes'],
]);
// Update detail transaksi
foreach ($validated['details'] as $detail) {
$detailData = [
'transaction_id' => $transaction->id,
'procedure_id' => $detail['procedure_id'],
'performed_by' => $detail['performed_by'],
'quantity' => $detail['quantity'],
'unit_price' => Procedure::findOrFail($detail['procedure_id'])->base_price,
'subtotal' => Procedure::findOrFail($detail['procedure_id'])->base_price * $detail['quantity'],
'notes' => $detail['notes'] ?? null,
];
// Update the first transaction detail or create a new one
$detail = TransactionDetail::where('transaction_id', $transaction->id)->first();
$detailData = [
'transaction_id' => $transaction->id,
'procedure_id' => $validated['procedure_id'],
'performed_by' => $validated['employee_id'],
'procedure_code' => $procedure->code,
'procedure_name' => $validated['service_name'],
'quantity' => 1, // Default quantity to 1
'unit_price' => $validated['subtotal'],
'discount_amount' => $discountAmount,
'tax_amount' => $validated['tax_amount'],
'subtotal' => $validated['subtotal'],
'notes' => $validated['notes'],
];
if (isset($detail['id']) && $detail['id']) {
TransactionDetail::where('id', $detail['id'])->update($detailData);
} else {
TransactionDetail::create($detailData);
}
if ($detail) {
$detail->update($detailData);
} else {
TransactionDetail::create($detailData);
}
// Update registration payment status if needed
$registration = Registration::findOrFail($validated['registration_id']);
if ($validated['status'] === 'paid') {
$registration->update(['payment_status' => 'paid']);
} elseif ($validated['status'] === 'partially_paid') {
$registration->update(['payment_status' => 'partial']);
}
// Redirect ke halaman transaksi dengan pesan sukses
return redirect()->route('transactions.index')
->with('status', 'Data transaksi berhasil diperbarui');
});
@ -429,7 +440,6 @@ class TransactionController extends Controller
{
return DB::transaction(function () use ($transaction) {
TransactionDetail::where('transaction_id', $transaction->id)->delete();
$transaction->delete();
return redirect()->route('transactions.index')

View File

@ -60,6 +60,11 @@ class Registration extends Model
'is_active' => true,
];
public function transaction()
{
return $this->hasOne(Transaction::class);
}
public function patient()
{
return $this->belongsTo(Patient::class, 'patient_id');

View File

@ -1,49 +1,57 @@
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useForm } from '@inertiajs/react';
import { Textarea } from '@/components/ui/textarea';
import AppLayout from '@/layouts/app-layout';
import { Head } from '@inertiajs/react';
import { cn } from '@/lib/utils';
import { type BreadcrumbItem } from '@/types';
import InputError from '@/components/input-error';
import { Head, useForm } from '@inertiajs/react';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Textarea } from '@/components/ui/textarea';
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
interface TransactionFormProps {
mode: 'create' | 'edit';
patients: Array<{ id: string; name: string }>;
registrations: Array<{ id: string; registration_number: string; patient_name: string }>;
insurances: Array<{ id: string; name: string }>;
procedures: Array<{ id: string; code: string; name: string }>;
procedures: Array<{
id: string;
code: string;
name: string;
base_price?: number;
tax_percentage?: number;
is_taxable?: boolean;
}>;
employees: Array<{ id: string; name: string }>;
paymentMethods: Record<string, string>;
statusOptions: Record<string, string>;
transaction?: {
id?: string;
transaction_number: string;
registration_id?: string;
patient_id?: string;
invoice_number: string;
registration_id: string;
patient_id: string;
insurance_id?: string;
procedure_id?: string;
employee_id?: string;
service_name: string;
description?: string;
transaction_datetime: string;
payment_datetime?: string;
due_date?: string;
subtotal: number;
tax_amount: number;
discount_amount: number;
grand_total: number;
payment_method: string;
status: string;
paid_amount: number;
insurance_covered_amount: number;
details?: string;
patient_responsibility: number;
payment_method: string;
payment_reference?: string;
status: string;
notes?: string;
};
}
@ -56,23 +64,22 @@ const breadcrumbs: BreadcrumbItem[] = [
const toLocalISOString = (date: Date) => {
const offset = date.getTimezoneOffset();
const localDate = new Date(date.getTime() - (offset * 60 * 1000));
return localDate.toISOString().split('T')[0];
const localDate = new Date(date.getTime() - offset * 60 * 1000);
return localDate.toISOString().slice(0, 16);
};
export default function TransactionForm({
mode,
patients,
registrations,
insurances,
procedures,
employees,
paymentMethods,
statusOptions,
transaction
}: TransactionFormProps) {
mode,
patients,
registrations,
insurances,
procedures,
employees,
paymentMethods,
statusOptions,
transaction,
}: TransactionFormProps) {
const { data, setData, post, put, processing, errors, reset } = useForm({
transaction_number: transaction?.transaction_number || '',
registration_id: transaction?.registration_id || '',
patient_id: transaction?.patient_id || '',
insurance_id: transaction?.insurance_id || '',
@ -80,40 +87,30 @@ export default function TransactionForm({
employee_id: transaction?.employee_id || '',
service_name: transaction?.service_name || '',
transaction_datetime: transaction?.transaction_datetime || toLocalISOString(new Date()),
payment_datetime: transaction?.payment_datetime || '',
due_date: transaction?.due_date || '',
subtotal: transaction?.subtotal || 0,
tax_amount: transaction?.tax_amount || 0,
discount_amount: transaction?.discount_amount || 0,
grand_total: transaction?.grand_total || 0,
payment_method: transaction?.payment_method || 'cash',
status: transaction?.status || 'pending',
paid_amount: transaction?.paid_amount || 0,
insurance_covered_amount: transaction?.insurance_covered_amount || 0,
patient_responsibility: transaction?.patient_responsibility || 0,
payment_method: transaction?.payment_method || 'cash',
payment_reference: transaction?.payment_reference || '',
status: transaction?.status || 'pending',
notes: transaction?.notes || '',
details: transaction?.details || '',
});
const [registrationSelected, setRegistrationSelected] = useState<boolean>(false);
// update pasien ketika memilih register
useEffect(() => {
const calculatedGrandTotal = data.subtotal + data.tax_amount - data.discount_amount;
setData('grand_total', calculatedGrandTotal);
if (data.payment_method === 'insurance') {
const insuranceCovered = calculatedGrandTotal * 0.8; // Example: insurance covers 80%
setData('insurance_covered_amount', insuranceCovered);
setData('paid_amount', calculatedGrandTotal - insuranceCovered);
} else {
setData('insurance_covered_amount', 0);
setData('paid_amount', calculatedGrandTotal);
}
}, [data.subtotal, data.tax_amount, data.discount_amount, data.payment_method]);
useEffect(() => {
if (data.registration_id) {
const selectedRegistration = registrations.find(r => r.id === data.registration_id);
if (data.registration_id && data.registration_id !== 'none') {
const selectedRegistration = registrations.find((r) => r.id === data.registration_id);
if (selectedRegistration) {
setRegistrationSelected(true);
const patient = patients.find(p => p.name === selectedRegistration.patient_name);
const patient = patients.find((p) => p.name === selectedRegistration.patient_name);
if (patient) {
setData('patient_id', patient.id);
}
@ -123,15 +120,76 @@ export default function TransactionForm({
}
}, [data.registration_id, registrations, patients]);
// Update ruangan, subtotal dan pajak ketika prosedur di select
useEffect(() => {
if (data.procedure_id) {
const selectedProcedure = procedures.find(p => p.id === data.procedure_id);
if (data.procedure_id && data.procedure_id !== 'manual') {
const selectedProcedure = procedures.find((p) => p.id === data.procedure_id);
if (selectedProcedure) {
setData('service_name', selectedProcedure.name);
const basePrice = selectedProcedure.base_price || 0;
const taxAmount =
selectedProcedure.is_taxable && selectedProcedure.tax_percentage ? basePrice * (selectedProcedure.tax_percentage / 100) : 0;
setData({
...data,
service_name: selectedProcedure.name,
subtotal: basePrice,
tax_amount: taxAmount,
});
}
}
}, [data.procedure_id, procedures]);
useEffect(() => {
// Pastikan semua nilai numerik valid (gunakan 0 jika NaN)
const subtotal = Number(data.subtotal) || 0;
const taxAmount = Number(data.tax_amount) || 0;
const discountAmount = Number(data.discount_amount) || 0;
const calculatedGrandTotal = subtotal + taxAmount - discountAmount;
let insuranceCovered = 0;
if (data.payment_method === 'insurance' && data.insurance_id) {
// Pastikan insurance covered tidak melebihi grand total
insuranceCovered = Math.min(calculatedGrandTotal, calculatedGrandTotal * 0.8);
}
const patientResponsibility = calculatedGrandTotal - insuranceCovered;
setData({
...data,
subtotal,
tax_amount: taxAmount,
discount_amount: discountAmount,
grand_total: calculatedGrandTotal,
insurance_covered_amount: insuranceCovered,
patient_responsibility: patientResponsibility,
// Update paid_amount hanya jika belum diisi atau jika nilainya tidak valid
paid_amount: Number(data.paid_amount) || patientResponsibility,
});
}, [data.subtotal, data.tax_amount, data.discount_amount, data.payment_method, data.insurance_id]);
// Calculate grand_total, insurance_covered_amount and patient_responsibility
useEffect(() => {
const calculatedGrandTotal = data.subtotal + data.tax_amount - data.discount_amount;
let insuranceCovered = 0;
if (data.payment_method === 'insurance' && data.insurance_id) {
// Example: insurance covers 80% of the total
insuranceCovered = calculatedGrandTotal * 0.8;
}
const patientResponsibility = calculatedGrandTotal - insuranceCovered;
setData({
...data,
grand_total: calculatedGrandTotal,
insurance_covered_amount: insuranceCovered,
patient_responsibility: patientResponsibility,
// Default paid amount to what patient is responsible for
paid_amount: data.paid_amount || patientResponsibility,
});
}, [data.subtotal, data.tax_amount, data.discount_amount, data.payment_method, data.insurance_id]);
const onSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (mode === 'create') {
@ -159,25 +217,17 @@ export default function TransactionForm({
<div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">
{mode === 'create' ? 'Tambah Transaksi Baru' : 'Edit Data Transaksi'}
</h1>
{mode === 'edit' && (
<span className="text-xl">
No. Transaksi {data.transaction_number}
</span>
)}
<h1 className="text-2xl font-bold">{mode === 'create' ? 'Tambah Transaksi Baru' : 'Edit Data Transaksi'}</h1>
{mode === 'edit' && transaction?.invoice_number && <span className="text-xl">No. Invoice: {transaction.invoice_number}</span>}
</div>
<div>
<form onSubmit={onSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{/* Registration Section */}
<div className="space-y-2">
<Label htmlFor="registration_id">No. Registrasi</Label>
<Select
value={data.registration_id}
onValueChange={(value) => setData('registration_id', value)}
>
<Select value={data.registration_id} onValueChange={(value) => setData('registration_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih nomor registrasi" />
</SelectTrigger>
@ -214,17 +264,15 @@ export default function TransactionForm({
<InputError message={errors.patient_id} />
</div>
{/* Procedure Section */}
<div className="space-y-2">
<Label htmlFor="procedure_id">Prosedur</Label>
<Select
value={data.procedure_id}
onValueChange={(value) => setData('procedure_id', value)}
>
<Select value={data.procedure_id} onValueChange={(value) => setData('procedure_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih prosedur" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">Manual Input</SelectItem>
<SelectItem value="manual">Input Manual</SelectItem>
{procedures.map((procedure) => (
<SelectItem key={procedure.id} value={procedure.id}>
{procedure.code} - {procedure.name}
@ -248,10 +296,7 @@ export default function TransactionForm({
<div className="space-y-2">
<Label htmlFor="employee_id">Pelaksana Layanan</Label>
<Select
value={data.employee_id}
onValueChange={(value) => setData('employee_id', value)}
>
<Select value={data.employee_id} onValueChange={(value) => setData('employee_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih pelaksana" />
</SelectTrigger>
@ -271,15 +316,15 @@ export default function TransactionForm({
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
variant={'outline'}
className={cn(
"w-full justify-start text-left font-normal",
!data.transaction_datetime && "text-muted-foreground"
'w-full justify-start text-left font-normal',
!data.transaction_datetime && 'text-muted-foreground',
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{data.transaction_datetime ? (
format(new Date(data.transaction_datetime), "dd/MM/yyyy HH:mm")
format(new Date(data.transaction_datetime), 'dd/MM/yyyy HH:mm')
) : (
<span>Pilih tanggal/waktu</span>
)}
@ -292,6 +337,17 @@ export default function TransactionForm({
onSelect={(date) => date && setData('transaction_datetime', toLocalISOString(date))}
initialFocus
/>
<div className="border-t p-3">
<Input
type="time"
value={data.transaction_datetime ? data.transaction_datetime.split('T')[1] || '' : ''}
onChange={(e) => {
const date =
data.transaction_datetime?.split('T')[0] || toLocalISOString(new Date()).split('T')[0];
setData('transaction_datetime', `${date}T${e.target.value}`);
}}
/>
</div>
</PopoverContent>
</Popover>
<InputError message={errors.transaction_datetime} />
@ -303,8 +359,11 @@ export default function TransactionForm({
<Input
id="subtotal"
type="number"
value={data.subtotal}
onChange={(e) => setData('subtotal', Math.max(0, parseFloat(e.target.value) || 0))}
value={isNaN(data.subtotal) ? '' : data.subtotal}
onChange={(e) => {
const value = parseFloat(e.target.value);
setData('subtotal', isNaN(value) ? 0 : value);
}}
placeholder="Subtotal"
/>
<InputError message={errors.subtotal} />
@ -323,7 +382,7 @@ export default function TransactionForm({
</div>
<div className="space-y-2">
<Label htmlFor="discount_amount">Diskon *</Label>
<Label htmlFor="discount_amount">Diskon</Label>
<Input
id="discount_amount"
type="number"
@ -336,28 +395,15 @@ export default function TransactionForm({
<div className="space-y-2">
<Label>Grand Total</Label>
<div className="p-2 font-semibold text-xl">
{formatCurrency(data.grand_total)}
<div className="flex h-9 items-center rounded border p-2 text-xl font-semibold">
{formatCurrency(isNaN(data.grand_total) ? 0 : data.grand_total)}
</div>
<InputError message={errors.grand_total} />
</div>
<div className="space-y-2">
<Label htmlFor="payment_method">Metode Pembayaran *</Label>
<Select
value={data.payment_method}
onValueChange ={(value) => {
setData('payment_method', value);
if (value === 'insurance' && data.insurance_id) {
const insuranceCovered = data.grand_total * 0.8;
setData('insurance_covered_amount', insuranceCovered);
setData('paid_amount', data.grand_total - insuranceCovered);
} else {
setData('insurance_covered_amount', 0);
setData('paid_amount', data.grand_total);
}
}}
>
<Select value={data.payment_method} onValueChange={(value) => setData('payment_method', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih metode pembayaran" />
</SelectTrigger>
@ -374,10 +420,7 @@ export default function TransactionForm({
<div className="space-y-2">
<Label htmlFor="status">Status Pembayaran *</Label>
<Select
value={data.status}
onValueChange={(value) => setData('status', value)}
>
<Select value={data.status} onValueChange={(value) => setData('status', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih status pembayaran" />
</SelectTrigger>
@ -396,15 +439,7 @@ export default function TransactionForm({
<>
<div className="space-y-2">
<Label htmlFor="insurance_id">Asuransi *</Label>
<Select
value={data.insurance_id}
onValueChange={(value) => {
setData('insurance_id', value);
const insuranceCovered = data.grand_total * 0.8;
setData('insurance_covered_amount', insuranceCovered);
setData('paid_amount', data.grand_total - insuranceCovered);
}}
>
<Select value={data.insurance_id} onValueChange={(value) => setData('insurance_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih asuransi" />
</SelectTrigger>
@ -427,8 +462,11 @@ export default function TransactionForm({
value={data.insurance_covered_amount}
onChange={(e) => {
const insuranceCovered = Math.max(0, parseFloat(e.target.value) || 0);
setData('insurance_covered_amount', insuranceCovered);
setData('paid_amount', data.grand_total - insuranceCovered);
setData({
...data,
insurance_covered_amount: insuranceCovered,
patient_responsibility: data.grand_total - insuranceCovered,
});
}}
placeholder="Jumlah dibayar asuransi"
/>
@ -437,6 +475,13 @@ export default function TransactionForm({
</>
)}
<div className="space-y-2">
<Label htmlFor="patient_responsibility">Tanggung Jawab Pasien</Label>
<div className="flex h-9 items-center rounded border p-2 font-semibold">
{formatCurrency(data.patient_responsibility)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="paid_amount">Jumlah Pembayaran *</Label>
<Input
@ -446,27 +491,65 @@ export default function TransactionForm({
onChange={(e) => {
const paidAmount = Math.max(0, parseFloat(e.target.value) || 0);
setData('paid_amount', paidAmount);
if (data.payment_method === 'insurance') {
setData('insurance_covered_amount', data.grand_total - paidAmount);
}
}}
placeholder="Jumlah pembayaran"
/>
<InputError message={errors.paid_amount} />
</div>
<div className="space-y-2 col-span-full">
<Label htmlFor="details">Detail Layanan *</Label>
<Textarea
id="details"
value={data.details}
onChange={(e) => setData('details', e.target.value)}
placeholder="Detail layanan yang diberikan"
<div className="space-y-2">
<Label htmlFor="payment_reference">Referensi Pembayaran</Label>
<Input
id="payment_reference"
value={data.payment_reference}
onChange={(e) => setData('payment_reference', e.target.value)}
placeholder="No. Kartu/No. Referensi/dll"
/>
<InputError message={errors.details} />
<InputError message={errors.payment_reference} />
</div>
<div className="space-y-2 col-span-full">
<div className="space-y-2">
<Label htmlFor="payment_datetime">Tanggal/Waktu Pembayaran</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-full justify-start text-left font-normal',
!data.payment_datetime && 'text-muted-foreground',
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{data.payment_datetime ? (
format(new Date(data.payment_datetime), 'dd/MM/yyyy HH:mm')
) : (
<span>Pilih tanggal/waktu pembayaran</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={data.payment_datetime ? new Date(data.payment_datetime) : undefined}
onSelect={(date) => date && setData('payment_datetime', toLocalISOString(date))}
initialFocus
/>
<div className="border-t p-3">
<Input
type="time"
value={data.payment_datetime ? data.payment_datetime.split('T')[1] || '' : ''}
onChange={(e) => {
const date = data.payment_datetime?.split('T')[0] || toLocalISOString(new Date()).split('T')[0];
setData('payment_datetime', `${date}T${e.target.value}`);
}}
/>
</div>
</PopoverContent>
</Popover>
<InputError message={errors.payment_datetime} />
</div>
<div className="col-span-full space-y-2">
<Label htmlFor="notes">Catatan</Label>
<Textarea
id="notes"
@ -479,7 +562,7 @@ export default function TransactionForm({
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" type="button" onClick ={() => window.history.back()}>
<Button variant="outline" type="button" onClick={() => window.history.back()}>
Batal
</Button>
<Button type="submit" disabled={processing}>