2026-01-20 09:44:05 +08:00
|
|
|
|
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";
|
2026-01-20 10:41:35 +08:00
|
|
|
|
import { Calendar } from "lucide-react";
|
2026-01-20 10:57:39 +08:00
|
|
|
|
import { getCurrentDate } from "@/utils/format";
|
2026-01-20 13:02:05 +08:00
|
|
|
|
import { validateInvoiceNumber } from "@/utils/validation";
|
2026-01-20 09:44:05 +08:00
|
|
|
|
|
|
|
|
|
|
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({
|
2026-01-20 10:57:39 +08:00
|
|
|
|
transaction_date: getCurrentDate(),
|
2026-01-20 09:44:05 +08:00
|
|
|
|
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({
|
2026-01-20 17:45:38 +08:00
|
|
|
|
transaction_date: fee.transaction_date,
|
2026-01-20 09:44:05 +08:00
|
|
|
|
category: fee.category,
|
|
|
|
|
|
amount: fee.amount.toString(),
|
|
|
|
|
|
invoice_number: fee.invoice_number || "",
|
|
|
|
|
|
description: fee.description || "",
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reset();
|
2026-01-20 10:57:39 +08:00
|
|
|
|
setData("transaction_date", getCurrentDate());
|
2026-01-20 09:44:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [open, fee]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
|
|
if (fee) {
|
2026-01-20 13:02:05 +08:00
|
|
|
|
const validation = validateInvoiceNumber(data.invoice_number);
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
|
toast.error(validation.error);
|
2026-01-20 10:57:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 09:44:05 +08:00
|
|
|
|
put(route("utility-fees.update", fee.id), {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
|
reset();
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("更新失敗,請檢查輸入資料");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2026-01-20 13:02:05 +08:00
|
|
|
|
const validation = validateInvoiceNumber(data.invoice_number);
|
|
|
|
|
|
if (!validation.isValid) {
|
|
|
|
|
|
toast.error(validation.error);
|
2026-01-20 10:57:39 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 09:44:05 +08:00
|
|
|
|
post(route("utility-fees.store"), {
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
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>
|
2026-01-20 10:41:35 +08:00
|
|
|
|
<div className="relative">
|
2026-01-20 10:57:39 +08:00
|
|
|
|
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" />
|
2026-01-20 10:41:35 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="transaction_date"
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={data.transaction_date}
|
|
|
|
|
|
onChange={(e) => setData("transaction_date", e.target.value)}
|
2026-01-20 10:57:39 +08:00
|
|
|
|
className={`pl-9 block w-full ${errors.transaction_date ? "border-red-500" : ""}`}
|
2026-01-20 10:41:35 +08:00
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2026-01-20 09:44:05 +08:00
|
|
|
|
{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"
|
2026-02-05 11:45:08 +08:00
|
|
|
|
step="any"
|
2026-01-20 09:44:05 +08:00
|
|
|
|
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}
|
2026-01-20 13:02:05 +08:00
|
|
|
|
onChange={(e) => setData("invoice_number", e.target.value.toUpperCase())}
|
2026-01-20 10:41:35 +08:00
|
|
|
|
placeholder="例:AB-12345678"
|
2026-01-20 13:02:05 +08:00
|
|
|
|
maxLength={11}
|
2026-01-20 09:44:05 +08:00
|
|
|
|
/>
|
2026-01-20 10:57:39 +08:00
|
|
|
|
<p className="text-xs text-gray-500">格式範例:AB-12345678</p>
|
2026-01-20 09:44:05 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|