Files
star-erp/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx

232 lines
9.3 KiB
TypeScript
Raw Normal View History

import { useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { useForm } from "@inertiajs/react";
import { toast } from "sonner";
import { Calendar } from "lucide-react";
import { getCurrentDate } from "@/utils/format";
import { validateInvoiceNumber } from "@/utils/validation";
export interface UtilityFee {
id: number;
transaction_date: string;
category: string;
amount: number | string;
invoice_number?: string;
description?: string;
created_at: string;
updated_at: string;
}
interface UtilityFeeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
fee: UtilityFee | null;
availableCategories: string[];
}
const DEFAULT_CATEGORIES = [
"電費",
"水費",
"瓦斯費",
"電話費",
"網路費",
"清潔費",
"管理費",
];
export default function UtilityFeeDialog({
open,
onOpenChange,
fee,
availableCategories,
}: UtilityFeeDialogProps) {
const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({
transaction_date: getCurrentDate(),
category: "",
amount: "",
invoice_number: "",
description: "",
});
// Combine default and available categories
const categories = Array.from(new Set([...DEFAULT_CATEGORIES, ...availableCategories]));
useEffect(() => {
if (open) {
clearErrors();
if (fee) {
setData({
transaction_date: fee.transaction_date,
category: fee.category,
amount: fee.amount.toString(),
invoice_number: fee.invoice_number || "",
description: fee.description || "",
});
} else {
reset();
setData("transaction_date", getCurrentDate());
}
}
}, [open, fee]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (fee) {
const validation = validateInvoiceNumber(data.invoice_number);
if (!validation.isValid) {
toast.error(validation.error);
return;
}
put(route("utility-fees.update", fee.id), {
onSuccess: () => {
toast.success("紀錄已更新");
onOpenChange(false);
reset();
},
onError: () => {
toast.error("更新失敗,請檢查輸入資料");
}
});
} else {
const validation = validateInvoiceNumber(data.invoice_number);
if (!validation.isValid) {
toast.error(validation.error);
return;
}
post(route("utility-fees.store"), {
onSuccess: () => {
toast.success("公共事業費已記錄");
onOpenChange(false);
reset();
},
onError: () => {
toast.error("紀錄失敗,請檢查輸入資料");
}
});
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{fee ? "編輯費用紀錄" : "新增費用紀錄"}</DialogTitle>
<DialogDescription>
{fee ? "修改此筆公共事業費的詳細資訊" : "記錄一筆新的公共事業費支出"}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 py-2">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label htmlFor="transaction_date">
<span className="text-red-500">*</span>
</Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
id="transaction_date"
type="date"
value={data.transaction_date}
onChange={(e) => setData("transaction_date", e.target.value)}
className={`pl-9 block w-full ${errors.transaction_date ? "border-red-500" : ""}`}
required
/>
</div>
{errors.transaction_date && <p className="text-sm text-red-500">{errors.transaction_date}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="category">
<span className="text-red-500">*</span>
</Label>
<SearchableSelect
value={data.category}
onValueChange={(value) => setData("category", value)}
options={categories.map((c) => ({ label: c, value: c }))}
placeholder="選擇或輸入類別"
searchPlaceholder="搜尋類別..."
className={errors.category ? "border-red-500" : ""}
/>
{errors.category && <p className="text-sm text-red-500">{errors.category}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="amount">
<span className="text-red-500">*</span>
</Label>
<Input
id="amount"
type="number"
step="0.01"
value={data.amount}
onChange={(e) => setData("amount", e.target.value)}
placeholder="0.00"
className={errors.amount ? "border-red-500" : ""}
required
/>
{errors.amount && <p className="text-sm text-red-500">{errors.amount}</p>}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="invoice_number"></Label>
<Input
id="invoice_number"
value={data.invoice_number}
onChange={(e) => setData("invoice_number", e.target.value.toUpperCase())}
placeholder="例AB-12345678"
maxLength={11}
/>
<p className="text-xs text-gray-500">AB-12345678</p>
{errors.invoice_number && <p className="text-sm text-red-500">{errors.invoice_number}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="description"> / </Label>
<Textarea
id="description"
value={data.description}
onChange={(e) => setData("description", e.target.value)}
placeholder="輸入其他備註資訊..."
className="resize-none"
/>
{errors.description && <p className="text-sm text-red-500">{errors.description}</p>}
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="button-outlined-primary"
>
</Button>
<Button type="submit" className="button-filled-primary" disabled={processing}>
{processing ? "處理中..." : (fee ? "儲存變更" : "確認紀錄")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}