Files
star-erp/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx
sky121113 8e0252e8fc
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s
[FEAT] 實作公共事業費附件上傳管理與更新 UI 協作規範防呆機制
2026-03-06 13:21:14 +08:00

266 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
billing_month: string;
category_id: number;
category?: string; // 相容於舊版或特定視圖
category_name?: string;
amount: number | string;
status: string;
payment_status?: 'pending' | 'paid' | 'overdue'; // 相容於舊版
due_date: string;
payment_date?: string;
transaction_date?: string; // 相容於舊版
invoice_number?: string;
description?: string;
attachments_count?: number;
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: "",
due_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 || "",
due_date: fee.due_date,
category: fee.category,
amount: fee.amount.toString(),
invoice_number: fee.invoice_number || "",
description: fee.description || "",
});
} else {
reset();
setData({
transaction_date: "",
due_date: getCurrentDate(),
category: "",
amount: "",
invoice_number: "",
description: "",
});
}
}
}, [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: () => {
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: () => {
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="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="due_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="due_date"
type="date"
value={data.due_date}
onChange={(e) => setData("due_date", e.target.value)}
className={`pl-9 block w-full ${errors.due_date ? "border-red-500" : ""}`}
required
/>
</div>
{errors.due_date && <p className="text-sm text-red-500">{errors.due_date}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="transaction_date">
</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" : ""}`}
/>
</div>
{errors.transaction_date && <p className="text-sm text-red-500">{errors.transaction_date}</p>}
</div>
</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="any"
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>
);
}