495 lines
25 KiB
TypeScript
495 lines
25 KiB
TypeScript
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { useForm } from '@inertiajs/react';
|
|
import AppLayout from '@/layouts/app-layout';
|
|
import { Head } from '@inertiajs/react';
|
|
import { type BreadcrumbItem } from '@/types';
|
|
import InputError from '@/components/input-error';
|
|
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';
|
|
|
|
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 }>;
|
|
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;
|
|
insurance_id?: string;
|
|
procedure_id?: string;
|
|
employee_id?: string;
|
|
service_name: string;
|
|
description?: string;
|
|
transaction_datetime: 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;
|
|
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().split('T')[0];
|
|
};
|
|
|
|
export default function TransactionForm({
|
|
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 || '',
|
|
procedure_id: transaction?.procedure_id || '',
|
|
employee_id: transaction?.employee_id || '',
|
|
service_name: transaction?.service_name || '',
|
|
transaction_datetime: transaction?.transaction_datetime || toLocalISOString(new 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,
|
|
notes: transaction?.notes || '',
|
|
details: transaction?.details || '',
|
|
});
|
|
|
|
const [registrationSelected, setRegistrationSelected] = useState<boolean>(false);
|
|
|
|
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 (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]);
|
|
|
|
useEffect(() => {
|
|
if (data.procedure_id) {
|
|
const selectedProcedure = procedures.find(p => p.id === data.procedure_id);
|
|
if (selectedProcedure) {
|
|
setData('service_name', selectedProcedure.name);
|
|
}
|
|
}
|
|
}, [data.procedure_id, procedures]);
|
|
|
|
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' && (
|
|
<span className="text-xl">
|
|
No. Transaksi {data.transaction_number}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<form onSubmit={onSubmit} className="space-y-6">
|
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
<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>
|
|
|
|
<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">Manual Input</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}
|
|
</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
|
|
/>
|
|
</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={data.subtotal}
|
|
onChange={(e) => setData('subtotal', Math.max(0, parseFloat(e.target.value) || 0))}
|
|
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="p-2 font-semibold text-xl">
|
|
{formatCurrency(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);
|
|
}
|
|
}}
|
|
>
|
|
<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);
|
|
const insuranceCovered = data.grand_total * 0.8;
|
|
setData('insurance_covered_amount', insuranceCovered);
|
|
setData('paid_amount', data.grand_total - insuranceCovered);
|
|
}}
|
|
>
|
|
<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('insurance_covered_amount', insuranceCovered);
|
|
setData('paid_amount', data.grand_total - insuranceCovered);
|
|
}}
|
|
placeholder="Jumlah dibayar asuransi"
|
|
/>
|
|
<InputError message={errors.insurance_covered_amount} />
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="paid_amount">Jumlah Pembayaran *</Label>
|
|
<Input
|
|
id="paid_amount"
|
|
type="number"
|
|
value={data.paid_amount}
|
|
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"
|
|
/>
|
|
<InputError message={errors.details} />
|
|
</div>
|
|
|
|
<div className="space-y-2 col-span-full">
|
|
<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>
|
|
);
|
|
}
|