Files
star-erp/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx
sky121113 3ce96537b3
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m0s
feat: 標準化全系統數值輸入欄位與擴充商品價格功能
1. UI 標準化:
   - 針對全系統數值輸入欄位統一加上 step='any' 以支援小數點。
   - 表格形式 (Table) 的數值輸入欄位統一加上 text-right 靠右對齊。
   - 修正 Components 與 Pages 中所有涉及金額與數量的輸入框。

2. 功能擴充與修正:
   - 擴充 Product 模型與相關 Dialog 以支援多種價格設定。
   - 修正 Inventory/GoodsReceipt/Create.tsx 未使用的變數錯誤。
   - 優化庫存相關頁面的 UI 一致性。

3. 其他:
   - 更新相關的 Type 定義與 Controller 邏輯。
2026-02-05 11:45:08 +08:00

230 lines
9.2 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;
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: () => {
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="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="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>
);
}