` 包裹返回按鈕與標題區塊
+3. **返回按鈕**:加上 `mb-4` 與標題區塊分隔
+4. **標題區塊**:使用 `
` 包裹 h1 和 p 標籤
+5. **標題樣式**:`text-2xl font-bold text-grey-0 flex items-center gap-2`
+6. **說明文字**:`text-gray-500 mt-1`
+
+### 範例頁面
+
+- ✅ `/resources/js/Pages/PurchaseOrder/Create.tsx`(建立採購單)
+- ✅ `/resources/js/Pages/Product/Create.tsx`(新增商品)
+- ✅ `/resources/js/Pages/Product/Edit.tsx`(編輯商品)
+
+---
+
## 4. 圖標規範
### 4.1 統一使用 lucide-react
diff --git a/app/Modules/Inventory/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php
index b4cfb61..4ba8ef5 100644
--- a/app/Modules/Inventory/Controllers/ProductController.php
+++ b/app/Modules/Inventory/Controllers/ProductController.php
@@ -113,57 +113,77 @@ class ProductController extends Controller
]);
}
+ /**
+ * 顯示建立表單。
+ */
+ public function create(): Response
+ {
+ return Inertia::render('Product/Create', [
+ 'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
+ 'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
+ ]);
+ }
+
/**
* 將新建立的資源儲存到儲存體中。
*/
public function store(Request $request)
{
$validated = $request->validate([
- 'code' => 'required|string|min:2|max:8|unique:products,code',
- 'barcode' => 'required|string|unique:products,barcode',
+ 'code' => 'nullable|unique:products,code',
+ 'barcode' => 'nullable|unique:products,barcode',
'name' => 'required|string|max:255',
'category_id' => 'required|exists:categories,id',
'brand' => 'nullable|string|max:255',
'specification' => 'nullable|string',
-
'base_unit_id' => 'required|exists:units,id',
'large_unit_id' => 'nullable|exists:units,id',
- 'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001',
- 'purchase_unit_id' => 'nullable|exists:units,id',
'purchase_unit_id' => 'nullable|exists:units,id',
+ 'conversion_rate' => 'nullable|numeric|min:0',
'location' => 'nullable|string|max:255',
'cost_price' => 'nullable|numeric|min:0',
'price' => 'nullable|numeric|min:0',
'member_price' => 'nullable|numeric|min:0',
'wholesale_price' => 'nullable|numeric|min:0',
- ], [
- 'code.required' => '商品代號為必填',
- 'code.max' => '商品代號最多 8 碼',
- 'code.min' => '商品代號最少 2 碼',
- 'code.unique' => '商品代號已存在',
- 'barcode.required' => '條碼編號為必填',
- 'barcode.unique' => '條碼編號已存在',
- 'name.required' => '商品名稱為必填',
- 'category_id.required' => '請選擇分類',
- 'category_id.exists' => '所選分類不存在',
- 'base_unit_id.required' => '基本庫存單位為必填',
- 'base_unit_id.exists' => '所選基本單位不存在',
- 'conversion_rate.required_with' => '填寫大單位時,換算率為必填',
- 'conversion_rate.numeric' => '換算率必須為數字',
- 'conversion_rate.min' => '換算率最小為 0.0001',
- 'cost_price.numeric' => '成本價必須為數字',
- 'cost_price.min' => '成本價不能小於 0',
- 'price.numeric' => '售價必須為數字',
- 'price.min' => '售價不能小於 0',
- 'member_price.numeric' => '會員價必須為數字',
- 'member_price.min' => '會員價不能小於 0',
- 'wholesale_price.numeric' => '批發價必須為數字',
- 'wholesale_price.min' => '批發價不能小於 0',
+ 'is_active' => 'boolean',
]);
-
+
+ if (empty($validated['code'])) {
+ $validated['code'] = $this->generateRandomCode();
+ }
+
$product = Product::create($validated);
- return redirect()->back()->with('success', '商品已建立');
+ return redirect()->route('products.index')->with('success', '商品已建立');
+ }
+
+ /**
+ * 顯示編輯表單。
+ */
+ public function edit(Product $product): Response
+ {
+ return Inertia::render('Product/Edit', [
+ 'product' => (object) [
+ 'id' => (string) $product->id,
+ 'code' => $product->code,
+ 'barcode' => $product->barcode,
+ 'name' => $product->name,
+ 'categoryId' => $product->category_id,
+ 'brand' => $product->brand,
+ 'specification' => $product->specification,
+ 'baseUnitId' => $product->base_unit_id,
+ 'largeUnitId' => $product->large_unit_id,
+ 'conversionRate' => (float) $product->conversion_rate,
+ 'purchaseUnitId' => $product->purchase_unit_id,
+ 'location' => $product->location,
+ 'cost_price' => (float) $product->cost_price,
+ 'price' => (float) $product->price,
+ 'member_price' => (float) $product->member_price,
+ 'wholesale_price' => (float) $product->wholesale_price,
+ ],
+ 'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
+ 'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
+ ]);
}
/**
@@ -172,50 +192,31 @@ class ProductController extends Controller
public function update(Request $request, Product $product)
{
$validated = $request->validate([
- 'code' => 'required|string|min:2|max:8|unique:products,code,' . $product->id,
- 'barcode' => 'required|string|unique:products,barcode,' . $product->id,
+ 'code' => 'nullable|unique:products,code,' . $product->id,
+ 'barcode' => 'nullable|unique:products,barcode,' . $product->id,
'name' => 'required|string|max:255',
'category_id' => 'required|exists:categories,id',
'brand' => 'nullable|string|max:255',
'specification' => 'nullable|string',
'base_unit_id' => 'required|exists:units,id',
'large_unit_id' => 'nullable|exists:units,id',
- 'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001',
- 'purchase_unit_id' => 'nullable|exists:units,id',
'purchase_unit_id' => 'nullable|exists:units,id',
+ 'conversion_rate' => 'nullable|numeric|min:0',
'location' => 'nullable|string|max:255',
'cost_price' => 'nullable|numeric|min:0',
'price' => 'nullable|numeric|min:0',
'member_price' => 'nullable|numeric|min:0',
'wholesale_price' => 'nullable|numeric|min:0',
- ], [
- 'code.required' => '商品代號為必填',
- 'code.max' => '商品代號最多 8 碼',
- 'code.min' => '商品代號最少 2 碼',
- 'code.unique' => '商品代號已存在',
- 'barcode.required' => '條碼編號為必填',
- 'barcode.unique' => '條碼編號已存在',
- 'name.required' => '商品名稱為必填',
- 'category_id.required' => '請選擇分類',
- 'category_id.exists' => '所選分類不存在',
- 'base_unit_id.required' => '基本庫存單位為必填',
- 'base_unit_id.exists' => '所選基本單位不存在',
- 'conversion_rate.required_with' => '填寫大單位時,換算率為必填',
- 'conversion_rate.numeric' => '換算率必須為數字',
- 'conversion_rate.min' => '換算率最小為 0.0001',
- 'cost_price.numeric' => '成本價必須為數字',
- 'cost_price.min' => '成本價不能小於 0',
- 'price.numeric' => '售價必須為數字',
- 'price.min' => '售價不能小於 0',
- 'member_price.numeric' => '會員價必須為數字',
- 'member_price.min' => '會員價不能小於 0',
- 'wholesale_price.numeric' => '批發價必須為數字',
- 'wholesale_price.min' => '批發價不能小於 0',
+ 'is_active' => 'boolean',
]);
+ if (empty($validated['code'])) {
+ $validated['code'] = $this->generateRandomCode();
+ }
+
$product->update($validated);
- return redirect()->back()->with('success', '商品已更新');
+ return redirect()->route('products.index')->with('success', '商品已更新');
}
/**
@@ -259,4 +260,22 @@ class ProductController extends Controller
return redirect()->back()->withErrors(['file' => '匯入失敗: ' . $e->getMessage()]);
}
}
+
+ /**
+ * 生成隨機 8 碼代號 (大寫英文+數字)
+ */
+ private function generateRandomCode(): string
+ {
+ $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ $code = '';
+
+ do {
+ $code = '';
+ for ($i = 0; $i < 8; $i++) {
+ $code .= $characters[rand(0, strlen($characters) - 1)];
+ }
+ } while (Product::where('code', $code)->exists());
+
+ return $code;
+ }
}
diff --git a/app/Modules/Inventory/Controllers/WarehouseController.php b/app/Modules/Inventory/Controllers/WarehouseController.php
index aca2356..000bf80 100644
--- a/app/Modules/Inventory/Controllers/WarehouseController.php
+++ b/app/Modules/Inventory/Controllers/WarehouseController.php
@@ -55,6 +55,19 @@ class WarehouseController extends Controller
->orWhere('expiry_date', '>=', now());
});
}], 'total_value')
+ ->withSum(['inventories as abnormal_amount' => function ($query) {
+ $query->where('quantity', '>', 0)
+ ->where(function ($q) {
+ $q->where('quality_status', '!=', 'normal')
+ ->orWhere(function ($sq) {
+ $sq->whereNotNull('expiry_date')
+ ->where('expiry_date', '<', now());
+ })
+ ->orWhereHas('warehouse', function ($wq) {
+ $wq->where('type', \App\Enums\WarehouseType::QUARANTINE);
+ });
+ });
+ }], 'total_value')
->addSelect(['low_stock_count' => function ($query) {
$query->selectRaw('count(*)')
->from('warehouse_product_safety_stocks as ss')
@@ -85,6 +98,17 @@ class WarehouseController extends Controller
$q->whereNull('expiry_date')
->orWhere('expiry_date', '>=', now());
})->sum('total_value'),
+ 'abnormal_amount' => \App\Modules\Inventory\Models\Inventory::where('quantity', '>', 0)
+ ->where(function ($q) {
+ $q->where('quality_status', '!=', 'normal')
+ ->orWhere(function ($sq) {
+ $sq->whereNotNull('expiry_date')
+ ->where('expiry_date', '<', now());
+ })
+ ->orWhereHas('warehouse', function ($wq) {
+ $wq->where('type', \App\Enums\WarehouseType::QUARANTINE);
+ });
+ })->sum('total_value'),
'book_stock' => \App\Modules\Inventory\Models\Inventory::sum('quantity'),
'book_amount' => \App\Modules\Inventory\Models\Inventory::sum('total_value'),
];
diff --git a/app/Modules/Inventory/Imports/ProductImport.php b/app/Modules/Inventory/Imports/ProductImport.php
index f881227..9836d32 100644
--- a/app/Modules/Inventory/Imports/ProductImport.php
+++ b/app/Modules/Inventory/Imports/ProductImport.php
@@ -63,8 +63,14 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
return null;
}
+ // 處理商品代號:若為空則自動生成
+ $code = $row['商品代號'] ?? null;
+ if (empty($code)) {
+ $code = $this->generateRandomCode();
+ }
+
return new Product([
- 'code' => $row['商品代號'],
+ 'code' => $code,
'barcode' => $row['條碼'],
'name' => $row['商品名稱'],
'category_id' => $categoryId,
@@ -81,10 +87,28 @@ class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapp
]);
}
+ /**
+ * 生成隨機 8 碼代號 (大寫英文+數字)
+ */
+ private function generateRandomCode(): string
+ {
+ $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ $code = '';
+
+ do {
+ $code = '';
+ for ($i = 0; $i < 8; $i++) {
+ $code .= $characters[rand(0, strlen($characters) - 1)];
+ }
+ } while (Product::where('code', $code)->exists());
+
+ return $code;
+ }
+
public function rules(): array
{
return [
- '商品代號' => ['required', 'string', 'min:2', 'max:8', 'unique:products,code'],
+ '商品代號' => ['nullable', 'string', 'min:2', 'max:8', 'unique:products,code'],
'條碼' => ['required', 'string', 'unique:products,barcode'],
'商品名稱' => ['required', 'string'],
'類別名稱' => ['required', function($attribute, $value, $fail) {
diff --git a/app/Modules/Inventory/Routes/web.php b/app/Modules/Inventory/Routes/web.php
index 27d7ec0..4e7dbd9 100644
--- a/app/Modules/Inventory/Routes/web.php
+++ b/app/Modules/Inventory/Routes/web.php
@@ -33,6 +33,8 @@ Route::middleware('auth')->group(function () {
Route::get('/products/template', [ProductController::class, 'template'])->name('products.template');
Route::post('/products/import', [ProductController::class, 'import'])->middleware('permission:products.create')->name('products.import');
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
+ Route::get('/products/create', [ProductController::class, 'create'])->middleware('permission:products.create')->name('products.create');
+ Route::get('/products/{product}/edit', [ProductController::class, 'edit'])->middleware('permission:products.edit')->name('products.edit');
Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store');
Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update');
Route::delete('/products/{product}', [ProductController::class, 'destroy'])->middleware('permission:products.delete')->name('products.destroy');
diff --git a/resources/js/Components/Product/ProductDialog.tsx b/resources/js/Components/Product/ProductDialog.tsx
deleted file mode 100644
index eea361a..0000000
--- a/resources/js/Components/Product/ProductDialog.tsx
+++ /dev/null
@@ -1,411 +0,0 @@
-import { useEffect } from "react";
-import { Wand2 } from "lucide-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 type { Product, Category } from "@/Pages/Product/Index";
-import type { Unit } from "@/Components/Unit/UnitManagerDialog";
-
-
-interface ProductDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- product: Product | null;
- categories: Category[];
- units: Unit[];
- onSave?: (product: any) => void; // Legacy prop, can be removed if fully switching to Inertia submit within dialog
-}
-
-export default function ProductDialog({
- open,
- onOpenChange,
- product,
- categories,
- units,
-}: ProductDialogProps) {
- const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({
- code: "",
- barcode: "",
- name: "",
- category_id: "",
- brand: "",
- specification: "",
- base_unit_id: "",
- large_unit_id: "",
- conversion_rate: "",
- purchase_unit_id: "",
- location: "",
- cost_price: "",
- price: "",
- member_price: "",
- wholesale_price: "",
- });
-
- useEffect(() => {
- if (open) {
- clearErrors();
- if (product) {
- setData({
- code: product.code,
- barcode: product.barcode || "",
- name: product.name,
- category_id: product.categoryId.toString(),
- brand: product.brand || "",
- specification: product.specification || "",
- base_unit_id: product.baseUnitId?.toString() || "",
- large_unit_id: product.largeUnitId?.toString() || "",
- conversion_rate: product.conversionRate ? product.conversionRate.toString() : "",
- purchase_unit_id: product.purchaseUnitId?.toString() || "",
- location: product.location || "",
- cost_price: product.cost_price?.toString() || "",
- price: product.price?.toString() || "",
- member_price: product.member_price?.toString() || "",
- wholesale_price: product.wholesale_price?.toString() || "",
- });
- } else {
- reset();
- // Set default category if available
- if (categories.length > 0) {
- setData("category_id", categories[0].id.toString());
- }
- }
- }
- }, [open, product, categories]);
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
-
- if (product) {
- put(route("products.update", product.id), {
- onSuccess: () => {
- onOpenChange(false);
- reset();
- },
- onError: () => {
- toast.error("更新失敗,請檢查輸入資料");
- }
- });
- } else {
- post(route("products.store"), {
- onSuccess: () => {
- onOpenChange(false);
- reset();
- },
- 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 (
-
- );
-}
\ No newline at end of file
diff --git a/resources/js/Components/Product/ProductForm.tsx b/resources/js/Components/Product/ProductForm.tsx
new file mode 100644
index 0000000..1a7779c
--- /dev/null
+++ b/resources/js/Components/Product/ProductForm.tsx
@@ -0,0 +1,331 @@
+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 (
+
+ );
+}
diff --git a/resources/js/Components/Product/ProductImportDialog.tsx b/resources/js/Components/Product/ProductImportDialog.tsx
index cc66e07..f04f23d 100644
--- a/resources/js/Components/Product/ProductImportDialog.tsx
+++ b/resources/js/Components/Product/ProductImportDialog.tsx
@@ -111,8 +111,9 @@ export default function ProductImportDialog({ open, onOpenChange }: ProductImpor
- - 必填欄位:商品代號 (2-8 碼)、條碼、商品名稱、類別名稱、基本單位。
- - 唯一性:商品代號與條碼不可與現有資料重複。
+ - 必填欄位:商品名稱、類別名稱、基本單位、條碼。
+ - 商品代號:2-8 碼,非必填(未填將自動生成,大寫英文+數字 8 碼)。
+ - 唯一性:商品代號(若有填寫)與條碼不可與現有資料重複。
- 自動關聯:類別與單位請填寫系統當前存在的「名稱」(如:飲品、瓶)。
- 大單位:若填寫大單位,則「換算率」為必填(需大於 0)。
diff --git a/resources/js/Components/Product/ProductTable.tsx b/resources/js/Components/Product/ProductTable.tsx
index d40bec5..808fa78 100644
--- a/resources/js/Components/Product/ProductTable.tsx
+++ b/resources/js/Components/Product/ProductTable.tsx
@@ -27,11 +27,11 @@ import {
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog";
import { Can } from "@/Components/Permission/Can";
+import { Link } from "@inertiajs/react";
import type { Product } from "@/Pages/Product/Index";
interface ProductTableProps {
products: Product[];
- onEdit: (product: Product) => void;
onDelete: (id: string) => void;
startIndex: number;
@@ -42,7 +42,6 @@ interface ProductTableProps {
export default function ProductTable({
products,
- onEdit,
onDelete,
startIndex,
sortField,
@@ -96,8 +95,8 @@ export default function ProductTable({
基本單位
-
規格
換算率
+
規格
儲位
操作
@@ -133,6 +132,15 @@ export default function ProductTable({
{product.baseUnit?.name || '-'}
+
+ {product.largeUnit ? (
+
+ 1 {product.largeUnit?.name} = {Number(product.conversionRate)} {product.baseUnit?.name}
+
+ ) : (
+ '-'
+ )}
+
@@ -149,15 +157,6 @@ export default function ProductTable({
-
- {product.largeUnit ? (
-
- 1 {product.largeUnit?.name} = {Number(product.conversionRate)} {product.baseUnit?.name}
-
- ) : (
- '-'
- )}
-
{product.location || '-'}
@@ -174,14 +173,16 @@ export default function ProductTable({
*/}
-
+
+
+
diff --git a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx
index 22b2ca5..c5fbe15 100644
--- a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx
+++ b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx
@@ -135,6 +135,12 @@ export default function InventoryTable({
{hasInventory ? `${group.batches.length} 個批號` : '無庫存'}
+ {group.batches.some(b => b.expiryDate && new Date(b.expiryDate) < new Date()) && (
+
+
+ 含過期項目
+
+ )}
@@ -217,7 +223,23 @@ export default function InventoryTable({
${batch.total_value?.toLocaleString()}
- {batch.expiryDate ? formatDate(batch.expiryDate) : "-"}
+ {batch.expiryDate ? (
+
+
+ {formatDate(batch.expiryDate)}
+
+ {new Date(batch.expiryDate) < new Date() && (
+
+
+
+
+
+ 此批號已過期
+
+
+ )}
+
+ ) : "-"}
{batch.lastInboundDate ? formatDate(batch.lastInboundDate) : "-"}
@@ -280,7 +302,7 @@ export default function InventoryTable({
})}
-
-
+
+
);
}
diff --git a/resources/js/Components/Warehouse/WarehouseCard.tsx b/resources/js/Components/Warehouse/WarehouseCard.tsx
index 0fe9c86..f02a1c4 100644
--- a/resources/js/Components/Warehouse/WarehouseCard.tsx
+++ b/resources/js/Components/Warehouse/WarehouseCard.tsx
@@ -23,6 +23,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
+import { Can } from "@/Components/Permission/Can";
interface WarehouseCardProps {
warehouse: Warehouse;
@@ -59,9 +60,12 @@ export default function WarehouseCard({
>
{/* 警告橫幅 */}
{hasWarning && (
-
-
-
低庫存警告
+
+
+
{stats.lowStockCount} 項
)}
@@ -81,12 +85,16 @@ export default function WarehouseCard({
-
+
{WAREHOUSE_TYPE_LABELS[warehouse.type || 'standard'] || '標準倉'}
+ {warehouse.type === 'quarantine' ? ' (不計入可用)' : ' (計入可用)'}
{warehouse.type === 'transit' && warehouse.license_plate && (
- {warehouse.license_plate}
+ {warehouse.license_plate} {warehouse.driver_name && `(${warehouse.driver_name})`}
)}
@@ -100,46 +108,36 @@ export default function WarehouseCard({
{/* 統計區塊 - 狀態標籤 */}
- {/* 銷售狀態與可用性說明 */}
-
-
庫存可用性
- {warehouse.type === 'quarantine' ? (
-
- 不計入可用
-
- ) : (
-
- 計入可用
-
+
+ {/* 帳面庫存總計 (金額) */}
+
+
+
+
+ ${Number(stats.totalValue || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
+
+
+ {/* 過期與瑕疵總計 (金額) */}
+
+ {Number(stats.abnormalValue || 0) > 0 && (
+
+
+
+ ${Number(stats.abnormalValue || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+
+
)}
-
+
- {/* 低庫存警告狀態 */}
-
-
-
- {hasWarning ? (
-
- {stats.lowStockCount} 項
-
- ) : (
-
- 正常
-
- )}
-
-
- {/* 移動倉司機資訊 */}
- {warehouse.type === 'transit' && warehouse.driver_name && (
-
- 司機
- {warehouse.driver_name}
-
- )}
diff --git a/resources/js/Pages/Product/Create.tsx b/resources/js/Pages/Product/Create.tsx
new file mode 100644
index 0000000..ae4adfe
--- /dev/null
+++ b/resources/js/Pages/Product/Create.tsx
@@ -0,0 +1,54 @@
+import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
+import { Head, Link } from "@inertiajs/react";
+import { Package, ArrowLeft } from "lucide-react";
+import { Button } from "@/Components/ui/button";
+import ProductForm from "@/Components/Product/ProductForm";
+import { getCreateBreadcrumbs } from "@/utils/breadcrumb";
+import type { Category } from "./Index";
+import type { Unit } from "@/Components/Unit/UnitManagerDialog";
+
+interface Props {
+ categories: Category[];
+ units: Unit[];
+}
+
+export default function Create({ categories, units }: Props) {
+ return (
+
+
+ );
+}
diff --git a/resources/js/Pages/Product/Edit.tsx b/resources/js/Pages/Product/Edit.tsx
new file mode 100644
index 0000000..cdc3eed
--- /dev/null
+++ b/resources/js/Pages/Product/Edit.tsx
@@ -0,0 +1,56 @@
+import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
+import { Head, Link } from "@inertiajs/react";
+import { Package, ArrowLeft } from "lucide-react";
+import { Button } from "@/Components/ui/button";
+import ProductForm from "@/Components/Product/ProductForm";
+import { getEditBreadcrumbs } from "@/utils/breadcrumb";
+import type { Category, Product } from "./Index";
+import type { Unit } from "@/Components/Unit/UnitManagerDialog";
+
+interface Props {
+ product: Product;
+ categories: Category[];
+ units: Unit[];
+}
+
+export default function Edit({ product, categories, units }: Props) {
+ return (
+
+
+ );
+}
diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx
index cdb9a0f..33708b5 100644
--- a/resources/js/Pages/Product/Index.tsx
+++ b/resources/js/Pages/Product/Index.tsx
@@ -4,12 +4,11 @@ import { Input } from "@/Components/ui/input";
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { Plus, Search, Package, X, Upload } from 'lucide-react';
import ProductTable from "@/Components/Product/ProductTable";
-import ProductDialog from "@/Components/Product/ProductDialog";
import ProductImportDialog from "@/Components/Product/ProductImportDialog";
import CategoryManagerDialog from "@/Components/Category/CategoryManagerDialog";
import UnitManagerDialog, { Unit } from "@/Components/Unit/UnitManagerDialog";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
-import { Head, router, usePage } from "@inertiajs/react";
+import { Head, router, usePage, Link } from "@inertiajs/react";
import { PageProps as GlobalPageProps } from "@/types/global";
import { debounce } from "lodash";
import Pagination from "@/Components/shared/Pagination";
@@ -70,11 +69,9 @@ export default function ProductManagement({ products, categories, units, filters
const [perPage, setPerPage] = useState