vchandra22 c5c8e44752
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
fix: bug fix on datepicker simply get year for dob
2025-04-27 21:56:28 +07:00

579 lines
30 KiB
TypeScript

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 { Textarea } from '@/components/ui/textarea';
import AppLayout from '@/layouts/app-layout';
import { cn } from '@/lib/utils';
import { type BreadcrumbItem } from '@/types';
import { Head, useForm } from '@inertiajs/react';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-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;
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;
invoice_number: string;
registration_id: string;
patient_id: string;
insurance_id?: string;
procedure_id?: string;
employee_id?: string;
service_name: string;
transaction_datetime: string;
payment_datetime?: string;
due_date?: string;
subtotal: number;
tax_amount: number;
discount_amount: number;
grand_total: number;
paid_amount: number;
insurance_covered_amount: number;
patient_responsibility: number;
payment_method: string;
payment_reference?: string;
status: string;
notes?: string;
};
}
const breadcrumbs: BreadcrumbItem[] = [
{ title: 'Dashboard', href: '/dashboard' },
{ title: 'Transaksi', href: '/transactions' },
{ title: 'Form', href: '#' },
];
const toLocalISOString = (date: Date) => {
const offset = date.getTimezoneOffset();
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) {
const { data, setData, post, put, processing, errors, reset } = useForm({
registration_id: transaction?.registration_id || '',
patient_id: transaction?.patient_id || '',
insurance_id: transaction?.insurance_id || '',
procedure_id: transaction?.procedure_id || '',
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,
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 || '',
});
const [registrationSelected, setRegistrationSelected] = useState<boolean>(false);
// update pasien ketika memilih register
useEffect(() => {
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);
if (patient) {
setData('patient_id', patient.id);
}
}
} else {
setRegistrationSelected(false);
}
}, [data.registration_id, registrations, patients]);
// Update ruangan, subtotal dan pajak ketika prosedur di select
useEffect(() => {
if (data.procedure_id && data.procedure_id !== 'manual') {
const selectedProcedure = procedures.find((p) => p.id === data.procedure_id);
if (selectedProcedure) {
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') {
post(route('transactions.store'), {
onSuccess: () => reset(),
});
} else {
put(route('transactions.update', transaction?.id), {
preserveScroll: true,
});
}
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value);
};
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title={`${mode === 'create' ? 'Tambah' : 'Edit'} Transaksi`} />
<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' && 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)}>
<SelectTrigger>
<SelectValue placeholder="Pilih nomor registrasi" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Tanpa registrasi</SelectItem>
{registrations.map((registration) => (
<SelectItem key={registration.id} value={registration.id}>
{registration.registration_number} - {registration.patient_name}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.registration_id} />
</div>
<div className="space-y-2">
<Label htmlFor="patient_id">Pasien *</Label>
<Select
value={data.patient_id}
onValueChange={(value) => setData('patient_id', value)}
disabled={registrationSelected}
>
<SelectTrigger>
<SelectValue placeholder="Pilih pasien" />
</SelectTrigger>
<SelectContent>
{patients.map((patient) => (
<SelectItem key={patient.id} value={patient.id}>
{patient.name}
</SelectItem>
))}
</SelectContent>
</Select>
<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)}>
<SelectTrigger>
<SelectValue placeholder="Pilih prosedur" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">Input Manual</SelectItem>
{procedures.map((procedure) => (
<SelectItem key={procedure.id} value={procedure.id}>
{procedure.code} - {procedure.name}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.procedure_id} />
</div>
<div className="space-y-2">
<Label htmlFor="service_name">Nama Layanan *</Label>
<Input
id="service_name"
value={data.service_name}
onChange={(e) => setData('service_name', e.target.value)}
placeholder="Nama layanan"
/>
<InputError message={errors.service_name} />
</div>
<div className="space-y-2">
<Label htmlFor="employee_id">Pelaksana Layanan</Label>
<Select value={data.employee_id} onValueChange={(value) => setData('employee_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih pelaksana" />
</SelectTrigger>
<SelectContent>
{employees.map((employee) => (
<SelectItem key={employee.id} value={employee.id}>
{employee.name} - {employee.specialization}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.employee_id} />
</div>
<div className="space-y-2">
<Label htmlFor="transaction_datetime">Tanggal/Waktu Transaksi *</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'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')
) : (
<span>Pilih tanggal/waktu</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={data.transaction_datetime ? new Date(data.transaction_datetime) : undefined}
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} />
</div>
{/* Financial Section */}
<div className="space-y-2">
<Label htmlFor="subtotal">Subtotal *</Label>
<Input
id="subtotal"
type="number"
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} />
</div>
<div className="space-y-2">
<Label htmlFor="tax_amount">Pajak *</Label>
<Input
id="tax_amount"
type="number"
value={data.tax_amount}
onChange={(e) => setData('tax_amount', Math.max(0, parseFloat(e.target.value) || 0))}
placeholder="Jumlah pajak"
/>
<InputError message={errors.tax_amount} />
</div>
<div className="space-y-2">
<Label htmlFor="discount_amount">Diskon</Label>
<Input
id="discount_amount"
type="number"
value={data.discount_amount}
onChange={(e) => setData('discount_amount', Math.max(0, parseFloat(e.target.value) || 0))}
placeholder="Jumlah diskon"
/>
<InputError message={errors.discount_amount} />
</div>
<div className="space-y-2">
<Label>Grand Total</Label>
<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)}>
<SelectTrigger>
<SelectValue placeholder="Pilih metode pembayaran" />
</SelectTrigger>
<SelectContent>
{Object.entries(paymentMethods).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.payment_method} />
</div>
<div className="space-y-2">
<Label htmlFor="status">Status Pembayaran *</Label>
<Select value={data.status} onValueChange={(value) => setData('status', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih status pembayaran" />
</SelectTrigger>
<SelectContent>
{Object.entries(statusOptions).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.status} />
</div>
{data.payment_method === 'insurance' && (
<>
<div className="space-y-2">
<Label htmlFor="insurance_id">Asuransi *</Label>
<Select value={data.insurance_id} onValueChange={(value) => setData('insurance_id', value)}>
<SelectTrigger>
<SelectValue placeholder="Pilih asuransi" />
</SelectTrigger>
<SelectContent>
{insurances.map((insurance) => (
<SelectItem key={insurance.id} value={insurance.id}>
{insurance.name}
</SelectItem>
))}
</SelectContent>
</Select>
<InputError message={errors.insurance_id} />
</div>
<div className="space-y-2">
<Label htmlFor="insurance_covered_amount">Dibayar Asuransi *</Label>
<Input
id="insurance_covered_amount"
type="number"
value={data.insurance_covered_amount}
onChange={(e) => {
const insuranceCovered = Math.max(0, parseFloat(e.target.value) || 0);
setData({
...data,
insurance_covered_amount: insuranceCovered,
patient_responsibility: data.grand_total - insuranceCovered,
});
}}
placeholder="Jumlah dibayar asuransi"
/>
<InputError message={errors.insurance_covered_amount} />
</div>
</>
)}
<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
id="paid_amount"
type="text"
value={formatCurrency(data.paid_amount)}
onChange={(e) => {
const numericValue = e.target.value.replace(/[^0-9]/g, '');
const paidAmount = parseFloat(numericValue) || 0;
setData('paid_amount', paidAmount);
}}
placeholder="Jumlah pembayaran"
/>
<InputError message={errors.paid_amount} />
</div>
<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.payment_reference} />
</div>
<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"
value={data.notes}
onChange={(e) => setData('notes', e.target.value)}
placeholder="Catatan tambahan"
/>
<InputError message={errors.notes} />
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" type="button" onClick={() => window.history.back()}>
Batal
</Button>
<Button type="submit" disabled={processing}>
{mode === 'create' ? 'Simpan' : 'Perbarui'} Transaksi
</Button>
</div>
</form>
</div>
</div>
</AppLayout>
);
}