Files
star-erp/resources/js/Components/Product/ProductForm.tsx

332 lines
15 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 { Wand2 } from "lucide-react";
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 type { Category, Product } from "@/Pages/Product/Index";
import type { Unit } from "@/Components/Unit/UnitManagerDialog";
interface ProductFormProps {
initialData?: Product | null;
categories: Category[];
units: Unit[];
onSubmitsuccess?: () => void;
}
export default function ProductForm({
initialData,
categories,
units,
}: ProductFormProps) {
const isEdit = !!initialData;
const { data, setData, post, put, processing, errors } = useForm({
code: initialData?.code || "",
barcode: initialData?.barcode || "",
name: initialData?.name || "",
category_id: initialData?.categoryId?.toString() || (categories.length > 0 ? categories[0].id.toString() : ""),
brand: initialData?.brand || "",
specification: initialData?.specification || "",
base_unit_id: initialData?.baseUnitId?.toString() || "",
large_unit_id: initialData?.largeUnitId?.toString() || "",
conversion_rate: initialData?.conversionRate?.toString() || "",
purchase_unit_id: initialData?.purchaseUnitId?.toString() || "",
location: initialData?.location || "",
cost_price: initialData?.cost_price?.toString() || "",
price: initialData?.price?.toString() || "",
member_price: initialData?.member_price?.toString() || "",
wholesale_price: initialData?.wholesale_price?.toString() || "",
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isEdit) {
put(route("products.update", initialData.id), {
onSuccess: () => toast.success("商品已更新"),
onError: () => toast.error("更新失敗,請檢查輸入資料"),
});
} else {
post(route("products.store"), {
onSuccess: () => toast.success("商品已建立"),
onError: () => toast.error("新增失敗,請檢查輸入資料"),
});
}
};
const generateRandomBarcode = () => {
const randomDigits = Math.floor(Math.random() * 9000000000) + 1000000000;
setData("barcode", randomDigits.toString());
};
const generateRandomCode = () => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let result = "";
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
setData("code", result);
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* 基本資訊 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6 space-y-6">
<div className="flex items-center justify-between border-b pb-2">
<h3 className="text-lg font-bold text-grey-0"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="category_id">
<span className="text-red-500">*</span>
</Label>
<SearchableSelect
value={data.category_id}
onValueChange={(value) => setData("category_id", value)}
options={categories.map((c) => ({ label: c.name, value: c.id.toString() }))}
placeholder="選擇分類"
className={errors.category_id ? "border-red-500" : ""}
/>
{errors.category_id && <p className="text-sm text-red-500">{errors.category_id}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="name">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={data.name}
onChange={(e) => setData("name", e.target.value)}
placeholder="例:法國麵粉"
className={errors.name ? "border-red-500 h-9" : "h-9"}
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="code">
<span className="text-gray-400 font-normal">()</span>
</Label>
<div className="flex gap-2">
<Input
id="code"
value={data.code}
onChange={(e) => setData("code", e.target.value)}
placeholder="例:A1 (未填將自動生成)"
maxLength={8}
className={`flex-1 h-9 ${errors.code ? "border-red-500" : ""}`}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={generateRandomCode}
title="隨機生成代號"
className="shrink-0 button-outlined-primary h-9 w-9"
>
<Wand2 className="h-4 w-4" />
</Button>
</div>
{errors.code && <p className="text-sm text-red-500">{errors.code}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="barcode">
<span className="text-red-500">*</span>
</Label>
<div className="flex gap-2">
<Input
id="barcode"
value={data.barcode}
onChange={(e) => setData("barcode", e.target.value)}
placeholder="輸入條碼或自動生成"
className={`flex-1 h-9 ${errors.barcode ? "border-red-500" : ""}`}
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={generateRandomBarcode}
title="隨機生成條碼"
className="shrink-0 button-outlined-primary h-9 w-9"
>
<Wand2 className="h-4 w-4" />
</Button>
</div>
{errors.barcode && <p className="text-sm text-red-500">{errors.barcode}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="brand"></Label>
<Input
id="brand"
value={data.brand}
onChange={(e) => setData("brand", e.target.value)}
placeholder="例:鳥越製粉"
className="h-9"
/>
</div>
<div className="space-y-2">
<Label htmlFor="location"></Label>
<Input
id="location"
value={data.location}
onChange={(e) => setData("location", e.target.value)}
placeholder="例A-1-1"
className="h-9"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="specification"></Label>
<Textarea
id="specification"
value={data.specification}
onChange={(e) => setData("specification", e.target.value)}
placeholder="例25kg/袋灰分0.45%"
className="resize-none"
/>
</div>
</div>
</div>
{/* 價格設定 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6 space-y-6">
<div className="flex items-center justify-between border-b pb-2">
<h3 className="text-lg font-bold text-grey-0"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<Label htmlFor="cost_price"></Label>
<Input
id="cost_price"
type="number"
step="any"
value={data.cost_price}
onChange={(e) => setData("cost_price", e.target.value)}
placeholder="0"
className="h-9 text-right"
/>
</div>
<div className="space-y-2">
<Label htmlFor="price"></Label>
<Input
id="price"
type="number"
step="any"
value={data.price}
onChange={(e) => setData("price", e.target.value)}
placeholder="0"
className="h-9 text-right"
/>
</div>
<div className="space-y-2">
<Label htmlFor="member_price"></Label>
<Input
id="member_price"
type="number"
step="any"
value={data.member_price}
onChange={(e) => setData("member_price", e.target.value)}
placeholder="0"
className="h-9 text-right"
/>
</div>
<div className="space-y-2">
<Label htmlFor="wholesale_price"></Label>
<Input
id="wholesale_price"
type="number"
step="any"
value={data.wholesale_price}
onChange={(e) => setData("wholesale_price", e.target.value)}
placeholder="0"
className="h-9 text-right"
/>
</div>
</div>
</div>
{/* 單位設定 */}
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-6 space-y-6">
<div className="flex items-center justify-between border-b pb-2">
<h3 className="text-lg font-bold text-grey-0"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label htmlFor="base_unit_id">
<span className="text-red-500">*</span>
</Label>
<SearchableSelect
value={data.base_unit_id}
onValueChange={(value) => setData("base_unit_id", value)}
options={units.map((u) => ({ label: u.name, value: u.id.toString() }))}
placeholder="選擇單位"
className={errors.base_unit_id ? "border-red-500" : ""}
/>
{errors.base_unit_id && <p className="text-sm text-red-500">{errors.base_unit_id}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="large_unit_id"> ()</Label>
<SearchableSelect
value={data.large_unit_id}
onValueChange={(value) => setData("large_unit_id", value)}
options={[
{ label: "無", value: "" },
...units.map((u) => ({ label: u.name, value: u.id.toString() }))
]}
placeholder="無"
/>
</div>
<div className="space-y-2">
<Label htmlFor="conversion_rate">
{data.large_unit_id && <span className="text-red-500">*</span>}
</Label>
<Input
id="conversion_rate"
type="number"
step="any"
value={data.conversion_rate}
onChange={(e) => setData("conversion_rate", e.target.value)}
placeholder={data.large_unit_id && data.base_unit_id ? `1 ${units.find(u => u.id.toString() === data.large_unit_id)?.name} = ? ${units.find(u => u.id.toString() === data.base_unit_id)?.name}` : "例如: 25"}
disabled={!data.large_unit_id}
className="h-9"
/>
{errors.conversion_rate && <p className="text-sm text-red-500">{errors.conversion_rate}</p>}
</div>
</div>
{data.large_unit_id && data.base_unit_id && data.conversion_rate && (
<div className="bg-primary-lightest p-3 rounded-lg text-sm text-primary-main font-medium border border-primary-light">
1 {units.find(u => u.id.toString() === data.large_unit_id)?.name} = {data.conversion_rate} {units.find(u => u.id.toString() === data.base_unit_id)?.name}
</div>
)}
</div>
{/* 提交按鈕 */}
<div className="flex justify-end pt-6 border-t">
<Button
type="submit"
className="button-filled-primary px-8"
disabled={processing}
>
{processing ? "處理中..." : (isEdit ? "儲存變更" : "建立商品")}
</Button>
</div>
</form>
);
}