Files
star-erp/resources/js/Pages/Production/Recipe/Create.tsx

309 lines
15 KiB
TypeScript

/**
* 新增配方頁面
*/
import { useEffect } from "react";
import { BookOpen, Plus, Trash2, ArrowLeft, Save } from 'lucide-react';
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, useForm, Link } from "@inertiajs/react";
import { getBreadcrumbs } from "@/utils/breadcrumb";
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
interface Product {
id: number;
name: string;
code: string;
base_unit_id?: number;
large_unit_id?: number;
}
interface Unit {
id: number;
name: string;
}
interface RecipeItem {
product_id: string;
quantity: string;
unit_id: string;
remark: string;
// UI Helpers
ui_product_name?: string;
ui_product_code?: string;
ui_unit_name?: string;
}
interface Props {
products: Product[];
units: Unit[];
}
export default function RecipeCreate({ products, units }: Props) {
const { data, setData, post, processing, errors } = useForm({
product_id: "",
code: "",
name: "",
description: "",
yield_quantity: "1",
items: [] as RecipeItem[],
});
// 自動產生配方名稱 (當選擇商品時)
useEffect(() => {
if (data.product_id && !data.name) {
const product = products.find(p => String(p.id) === data.product_id);
if (product) {
setData(d => ({ ...d, name: `${product.name} 標準配方` }));
}
}
// 自動產生代號 (簡易版)
if (data.product_id && !data.code) {
const product = products.find(p => String(p.id) === data.product_id);
if (product) {
setData(d => ({ ...d, code: `REC-${product.code}` }));
}
}
}, [data.product_id]);
const addItem = () => {
setData('items', [
...data.items,
{ product_id: "", quantity: "1", unit_id: "", remark: "" }
]);
};
const removeItem = (index: number) => {
setData('items', data.items.filter((_, i) => i !== index));
};
const updateItem = (index: number, field: keyof RecipeItem, value: string) => {
const newItems = [...data.items];
newItems[index] = { ...newItems[index], [field]: value };
// Auto-fill unit when product selected
if (field === 'product_id') {
const product = products.find(p => String(p.id) === value);
if (product) {
newItems[index].ui_product_name = product.name;
newItems[index].ui_product_code = product.code;
// Default to base unit and fix it
if (product.base_unit_id) {
newItems[index].unit_id = String(product.base_unit_id);
const unit = units.find(u => u.id === product.base_unit_id);
newItems[index].ui_unit_name = unit?.name;
}
}
}
setData('items', newItems);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
post(route('recipes.store'));
};
return (
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("recipes", [{ label: "新增", isPage: true }])}>
<Head title="新增配方" />
<div className="container mx-auto p-6 max-w-7xl">
<div className="mb-6">
<Link href={route('recipes.index')}>
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<BookOpen className="h-6 w-6 text-primary-main" />
</h1>
<p className="text-gray-500 mt-1">
</p>
</div>
<Button
onClick={handleSubmit}
disabled={processing}
className="button-filled-primary gap-2"
>
<Save className="h-4 w-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 左側:基本資料 */}
<div className="lg:col-span-1 space-y-6">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="space-y-4">
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"> *</Label>
<SearchableSelect
value={data.product_id}
onValueChange={(v) => setData('product_id', v)}
options={products.map(p => ({
label: `${p.name} (${p.code})`,
value: String(p.id),
}))}
placeholder="選擇商品"
className="w-full"
/>
{errors.product_id && <p className="text-red-500 text-xs">{errors.product_id}</p>}
</div>
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"> *</Label>
<Input
value={data.code}
onChange={(e) => setData('code', e.target.value)}
placeholder="例如: REC-P001"
/>
{errors.code && <p className="text-red-500 text-xs">{errors.code}</p>}
</div>
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"> *</Label>
<Input
value={data.name}
onChange={(e) => setData('name', e.target.value)}
placeholder="例如: 草莓冰標準配方"
/>
{errors.name && <p className="text-red-500 text-xs">{errors.name}</p>}
</div>
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"> *</Label>
<div className="flex items-center gap-2">
<Input
type="number"
value={data.yield_quantity}
onChange={(e) => setData('yield_quantity', e.target.value)}
placeholder="1"
/>
<span className="text-sm text-gray-500"></span>
</div>
{errors.yield_quantity && <p className="text-red-500 text-xs">{errors.yield_quantity}</p>}
</div>
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<Textarea
value={data.description}
onChange={(e) => setData('description', e.target.value)}
placeholder="備註說明..."
rows={3}
/>
</div>
</div>
</div>
</div>
{/* 右側:配方明細 */}
<div className="lg:col-span-2">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200 h-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold"> (BOM)</h2>
<Button
type="button"
variant="outline"
onClick={addItem}
className="gap-2 button-filled-primary text-white"
>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[35%]"></TableHead>
<TableHead className="w-[20%]"></TableHead>
<TableHead className="w-[20%]"></TableHead>
<TableHead className="w-[20%]"></TableHead>
<TableHead className="w-[5%]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="h-24 text-center text-gray-500">
</TableCell>
</TableRow>
) : (
data.items.map((item, index) => (
<TableRow key={index}>
<TableCell className="align-top">
<SearchableSelect
value={item.product_id}
onValueChange={(v) => updateItem(index, 'product_id', v)}
options={products.map(p => ({
label: `${p.name} (${p.code})`,
value: String(p.id)
}))}
placeholder="選擇原料"
className="w-full"
/>
</TableCell>
<TableCell className="align-top">
<Input
type="number"
step="0.0001"
value={item.quantity}
onChange={(e) => updateItem(index, 'quantity', e.target.value)}
placeholder="數量"
/>
</TableCell>
<TableCell className="align-middle">
<div className="text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-md border border-gray-100 min-h-[38px] flex items-center">
{item.ui_unit_name || (units.find(u => String(u.id) === item.unit_id)?.name) || '-'}
</div>
</TableCell>
<TableCell className="align-top">
<Input
value={item.remark}
onChange={(e) => updateItem(index, 'remark', e.target.value)}
placeholder="備註"
/>
</TableCell>
<TableCell className="align-top">
<Button
variant="ghost"
size="sm"
onClick={() => removeItem(index)}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{errors.items && <p className="text-red-500 text-sm mt-2">{errors.items}</p>}
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}