feat: 倉庫業務屬性、庫存成本追蹤與採購單功能更新
1. 倉庫管理:新增業務類型 (Owned/External/Customer) 與車牌資訊與司機欄位。 2. 庫存管理:實作成本追蹤 (unit_cost, total_value),更新列表與撥補單顯示。 3. 採購單:新增採購日期 (order_date),調整欄位名稱與順序。 4. 前端優化:更新相關 TS Type 定義與 UI 顯示。
This commit is contained in:
@@ -41,11 +41,11 @@ export function PurchaseOrderItemsTable({
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50 hover:bg-gray-50">
|
||||
<TableHead className="w-[20%] text-left">商品名稱</TableHead>
|
||||
<TableHead className="w-[10%] text-left">數量</TableHead>
|
||||
<TableHead className="w-[10%] text-left">採購數量</TableHead>
|
||||
<TableHead className="w-[12%] text-left">單位</TableHead>
|
||||
<TableHead className="w-[12%] text-left">換算基本單位</TableHead>
|
||||
<TableHead className="w-[15%] text-left">小計</TableHead>
|
||||
<TableHead className="w-[15%] text-left">單價 / 基本單位</TableHead>
|
||||
<TableHead className="w-[15%] text-left">小計</TableHead>
|
||||
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -146,7 +146,30 @@ export function PurchaseOrderItemsTable({
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 總金額 (主要輸入欄位) */}
|
||||
{/* 換算採購單價 / 基本單位 (顯示換算結果 - SWAPPED HERE) */}
|
||||
<TableCell className="text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-gray-500 font-medium text-sm">
|
||||
{formatCurrency(convertedUnitPrice)} / {item.base_unit_name || "個"}
|
||||
</div>
|
||||
{convertedUnitPrice > 0 && item.previousPrice && item.previousPrice > 0 && (
|
||||
<>
|
||||
{convertedUnitPrice > item.previousPrice && (
|
||||
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
|
||||
⚠️ 高於上次: {formatCurrency(item.previousPrice)}
|
||||
</p>
|
||||
)}
|
||||
{convertedUnitPrice < item.previousPrice && (
|
||||
<p className="text-[10px] text-green-600 font-medium">
|
||||
📉 低於上次: {formatCurrency(item.previousPrice)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 總金額 (主要輸入欄位 - SWAPPED HERE) */}
|
||||
<TableCell className="text-left">
|
||||
{isReadOnly ? (
|
||||
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
|
||||
@@ -178,29 +201,6 @@ export function PurchaseOrderItemsTable({
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 換算採購單價 / 基本單位 (顯示換算結果) */}
|
||||
<TableCell className="text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-gray-500 font-medium text-sm">
|
||||
{formatCurrency(convertedUnitPrice)} / {item.base_unit_name || "個"}
|
||||
</div>
|
||||
{convertedUnitPrice > 0 && item.previousPrice && item.previousPrice > 0 && (
|
||||
<>
|
||||
{convertedUnitPrice > item.previousPrice && (
|
||||
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
|
||||
⚠️ 高於上次: {formatCurrency(item.previousPrice)}
|
||||
</p>
|
||||
)}
|
||||
{convertedUnitPrice < item.previousPrice && (
|
||||
<p className="text-[10px] text-green-600 font-medium">
|
||||
📉 低於上次: {formatCurrency(item.previousPrice)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 刪除按鈕 */}
|
||||
{!isReadOnly && onRemoveItem && (
|
||||
<TableCell className="text-center">
|
||||
|
||||
@@ -147,14 +147,21 @@ export default function InventoryTable({
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">
|
||||
總庫存:<span className={`font-medium ${isLowStock ? "text-red-600" : "text-gray-900"}`}>{totalQuantity} 個</span>
|
||||
總庫存:<span className={`font-medium ${isLowStock ? "text-red-600" : "text-gray-900"}`}>{totalQuantity} {group.baseUnit}</span>
|
||||
</span>
|
||||
</div>
|
||||
<Can permission="inventory.view_cost">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">
|
||||
總價值:<span className="font-medium text-gray-900">${group.totalValue?.toLocaleString()}</span>
|
||||
</span>
|
||||
</div>
|
||||
</Can>
|
||||
{group.safetyStock !== null ? (
|
||||
<>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">
|
||||
安全庫存:<span className="font-medium text-gray-900">{group.safetyStock} 個</span>
|
||||
安全庫存:<span className="font-medium text-gray-900">{group.safetyStock} {group.baseUnit}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -193,11 +200,14 @@ export default function InventoryTable({
|
||||
<TableRow>
|
||||
<TableHead className="w-[5%]">#</TableHead>
|
||||
<TableHead className="w-[12%]">批號</TableHead>
|
||||
<TableHead className="w-[12%]">庫存數量</TableHead>
|
||||
<TableHead className="w-[15%]">進貨編號</TableHead>
|
||||
<TableHead className="w-[14%]">保存期限</TableHead>
|
||||
<TableHead className="w-[14%]">最新入庫</TableHead>
|
||||
<TableHead className="w-[14%]">最新出庫</TableHead>
|
||||
<TableHead className="w-[10%]">庫存數量</TableHead>
|
||||
<Can permission="inventory.view_cost">
|
||||
<TableHead className="w-[10%]">單位成本</TableHead>
|
||||
<TableHead className="w-[10%]">總價值</TableHead>
|
||||
</Can>
|
||||
<TableHead className="w-[12%]">保存期限</TableHead>
|
||||
<TableHead className="w-[12%]">最新入庫</TableHead>
|
||||
<TableHead className="w-[12%]">最新出庫</TableHead>
|
||||
<TableHead className="w-[8%] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -208,9 +218,12 @@ export default function InventoryTable({
|
||||
<TableCell className="text-grey-2">{index + 1}</TableCell>
|
||||
<TableCell>{batch.batchNumber || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<span>{batch.quantity}</span>
|
||||
<span>{batch.quantity} {batch.unit}</span>
|
||||
</TableCell>
|
||||
<TableCell>{batch.batchNumber || "-"}</TableCell>
|
||||
<Can permission="inventory.view_cost">
|
||||
<TableCell>${batch.unit_cost?.toLocaleString()}</TableCell>
|
||||
<TableCell>${batch.total_value?.toLocaleString()}</TableCell>
|
||||
</Can>
|
||||
<TableCell>
|
||||
{batch.expiryDate ? formatDate(batch.expiryDate) : "-"}
|
||||
</TableCell>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Textarea } from "@/Components/ui/textarea";
|
||||
import { toast } from "sonner";
|
||||
import { Warehouse, TransferOrder, TransferOrderStatus } from "@/types/warehouse";
|
||||
import { validateTransferOrder, validateTransferQuantity } from "@/utils/validation";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
|
||||
export type { TransferOrder };
|
||||
|
||||
@@ -42,6 +43,8 @@ interface AvailableProduct {
|
||||
availableQty: number;
|
||||
unit: string;
|
||||
expiryDate: string | null;
|
||||
unitCost: number; // 新增
|
||||
totalValue: number; // 新增
|
||||
}
|
||||
|
||||
export default function TransferOrderDialog({
|
||||
@@ -52,6 +55,9 @@ export default function TransferOrderDialog({
|
||||
// inventories,
|
||||
onSave,
|
||||
}: TransferOrderDialogProps) {
|
||||
const { can } = usePermission();
|
||||
const canViewCost = can('inventory.view_cost');
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
sourceWarehouseId: "",
|
||||
targetWarehouseId: "",
|
||||
@@ -106,7 +112,9 @@ export default function TransferOrderDialog({
|
||||
batchNumber: item.batch_number,
|
||||
availableQty: item.quantity,
|
||||
unit: item.unit_name,
|
||||
expiryDate: item.expiry_date
|
||||
expiryDate: item.expiry_date,
|
||||
unitCost: item.unit_cost, // 映射
|
||||
totalValue: item.total_value, // 映射
|
||||
}));
|
||||
setAvailableProducts(mappedData);
|
||||
})
|
||||
@@ -249,7 +257,7 @@ export default function TransferOrderDialog({
|
||||
onValueChange={handleProductChange}
|
||||
disabled={!formData.sourceWarehouseId || !!order}
|
||||
options={availableProducts.map((product) => ({
|
||||
label: `${product.productName} | 批號: ${product.batchNumber || '-'} | 效期: ${product.expiryDate || '-'} (庫存: ${product.availableQty} ${product.unit})`,
|
||||
label: `${product.productName} | 批號: ${product.batchNumber || '-'} | 效期: ${product.expiryDate || '-'} (庫存: ${product.availableQty} ${product.unit})${canViewCost ? ` | 成本: $${product.unitCost?.toLocaleString()}` : ''}`,
|
||||
value: `${product.productId}|||${product.batchNumber}`,
|
||||
}))}
|
||||
placeholder="選擇商品與批號"
|
||||
|
||||
@@ -32,6 +32,15 @@ interface WarehouseCardProps {
|
||||
onEdit: (warehouse: Warehouse) => void;
|
||||
}
|
||||
|
||||
const WAREHOUSE_TYPE_LABELS: Record<string, string> = {
|
||||
standard: "標準倉",
|
||||
production: "生產倉",
|
||||
retail: "門市倉",
|
||||
vending: "販賣機",
|
||||
transit: "在途倉",
|
||||
quarantine: "瑕疵倉",
|
||||
};
|
||||
|
||||
export default function WarehouseCard({
|
||||
warehouse,
|
||||
stats,
|
||||
@@ -71,6 +80,16 @@ export default function WarehouseCard({
|
||||
<Info className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{WAREHOUSE_TYPE_LABELS[warehouse.type || 'standard'] || '標準倉'}
|
||||
</Badge>
|
||||
{warehouse.type === 'transit' && warehouse.license_plate && (
|
||||
<Badge variant="secondary" className="text-xs font-normal bg-yellow-100 text-yellow-800 border-yellow-200">
|
||||
{warehouse.license_plate}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,6 +126,14 @@ export default function WarehouseCard({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移動倉司機資訊 */}
|
||||
{warehouse.type === 'transit' && warehouse.driver_name && (
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<span className="text-sm text-gray-500">司機</span>
|
||||
<span className="text-sm font-medium text-gray-900">{warehouse.driver_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 倉庫對話框元件
|
||||
* 重構後:加入驗證邏輯
|
||||
* 重構後:加入驗證邏輯與業務類型支援
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -27,9 +27,10 @@ import { Label } from "@/Components/ui/label";
|
||||
import { Textarea } from "@/Components/ui/textarea";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Warehouse } from "@/types/warehouse";
|
||||
import { Warehouse, WarehouseType } from "@/types/warehouse";
|
||||
import { validateWarehouse } from "@/utils/validation";
|
||||
import { toast } from "sonner";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
|
||||
interface WarehouseDialogProps {
|
||||
open: boolean;
|
||||
@@ -39,6 +40,15 @@ interface WarehouseDialogProps {
|
||||
onDelete?: (warehouseId: string) => void;
|
||||
}
|
||||
|
||||
const WAREHOUSE_TYPE_OPTIONS: { label: string; value: WarehouseType }[] = [
|
||||
{ label: "標準倉 (總倉)", value: "standard" },
|
||||
{ label: "生產倉 (廚房/加工)", value: "production" },
|
||||
{ label: "門市倉 (前台销售)", value: "retail" },
|
||||
{ label: "販賣機 (IoT設備)", value: "vending" },
|
||||
{ label: "在途倉 (物流車)", value: "transit" },
|
||||
{ label: "瑕疵倉 (報廢/檢驗)", value: "quarantine" },
|
||||
];
|
||||
|
||||
export default function WarehouseDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -51,13 +61,19 @@ export default function WarehouseDialog({
|
||||
name: string;
|
||||
address: string;
|
||||
description: string;
|
||||
type: WarehouseType;
|
||||
is_sellable: boolean;
|
||||
license_plate: string;
|
||||
driver_name: string;
|
||||
}>({
|
||||
code: "",
|
||||
name: "",
|
||||
address: "",
|
||||
description: "",
|
||||
type: "standard",
|
||||
is_sellable: true,
|
||||
license_plate: "",
|
||||
driver_name: "",
|
||||
});
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
@@ -69,7 +85,10 @@ export default function WarehouseDialog({
|
||||
name: warehouse.name,
|
||||
address: warehouse.address || "",
|
||||
description: warehouse.description || "",
|
||||
type: warehouse.type || "standard",
|
||||
is_sellable: warehouse.is_sellable ?? true,
|
||||
license_plate: warehouse.license_plate || "",
|
||||
driver_name: warehouse.driver_name || "",
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
@@ -77,7 +96,10 @@ export default function WarehouseDialog({
|
||||
name: "",
|
||||
address: "",
|
||||
description: "",
|
||||
type: "standard",
|
||||
is_sellable: true,
|
||||
license_plate: "",
|
||||
driver_name: "",
|
||||
});
|
||||
}
|
||||
}, [warehouse, open]);
|
||||
@@ -136,8 +158,21 @@ export default function WarehouseDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 倉庫名稱 */}
|
||||
{/* 倉庫類型 */}
|
||||
<div className="space-y-2">
|
||||
<Label>倉庫類型 <span className="text-red-500">*</span></Label>
|
||||
<SearchableSelect
|
||||
value={formData.type}
|
||||
onValueChange={(val) => setFormData({ ...formData, type: val as WarehouseType })}
|
||||
options={WAREHOUSE_TYPE_OPTIONS}
|
||||
placeholder="選擇倉庫類型"
|
||||
className="h-9"
|
||||
showSearch={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 倉庫名稱 */}
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label htmlFor="name">
|
||||
倉庫名稱 <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
@@ -147,11 +182,43 @@ export default function WarehouseDialog({
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="例:中央倉庫"
|
||||
required
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移動倉專屬資訊 */}
|
||||
{formData.type === 'transit' && (
|
||||
<div className="space-y-4 bg-yellow-50 p-4 rounded-lg border border-yellow-100">
|
||||
<div className="border-b border-yellow-200 pb-2">
|
||||
<h4 className="text-sm text-yellow-800 font-medium">車輛資訊 (在途倉)</h4>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="license_plate">車牌號碼</Label>
|
||||
<Input
|
||||
id="license_plate"
|
||||
value={formData.license_plate}
|
||||
onChange={(e) => setFormData({ ...formData, license_plate: e.target.value })}
|
||||
placeholder="例:ABC-1234"
|
||||
className="h-9 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="driver_name">司機姓名</Label>
|
||||
<Input
|
||||
id="driver_name"
|
||||
value={formData.driver_name}
|
||||
onChange={(e) => setFormData({ ...formData, driver_name: e.target.value })}
|
||||
placeholder="例:王小明"
|
||||
className="h-9 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 銷售設定 */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b pb-2">
|
||||
@@ -167,6 +234,9 @@ export default function WarehouseDialog({
|
||||
/>
|
||||
<Label htmlFor="is_sellable">此倉庫可進行銷售扣庫</Label>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 ml-6">
|
||||
啟用後,該倉庫庫存可用於 POS 或訂單銷售扣減。總倉通常不啟用,門市與行動販賣車需啟用。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 區塊 B:位置 */}
|
||||
@@ -186,6 +256,7 @@ export default function WarehouseDialog({
|
||||
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
|
||||
placeholder="例:台北市信義區信義路五段7號"
|
||||
required
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export default function CreatePurchaseOrder({
|
||||
const {
|
||||
supplierId,
|
||||
expectedDate,
|
||||
orderDate,
|
||||
items,
|
||||
notes,
|
||||
selectedSupplier,
|
||||
@@ -46,6 +47,7 @@ export default function CreatePurchaseOrder({
|
||||
warehouseId,
|
||||
setSupplierId,
|
||||
setExpectedDate,
|
||||
setOrderDate,
|
||||
setNotes,
|
||||
setWarehouseId,
|
||||
addItem,
|
||||
@@ -87,6 +89,11 @@ export default function CreatePurchaseOrder({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!orderDate) {
|
||||
toast.error("請選擇採購日期");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expectedDate) {
|
||||
toast.error("請選擇預計到貨日期");
|
||||
return;
|
||||
@@ -120,6 +127,7 @@ export default function CreatePurchaseOrder({
|
||||
const data = {
|
||||
vendor_id: supplierId,
|
||||
warehouse_id: warehouseId,
|
||||
order_date: orderDate,
|
||||
expected_delivery_date: expectedDate,
|
||||
remark: notes,
|
||||
status: status,
|
||||
@@ -235,6 +243,18 @@ export default function CreatePurchaseOrder({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">
|
||||
採購日期 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={orderDate || ""}
|
||||
onChange={(e) => setOrderDate(e.target.value)}
|
||||
className="block w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-bold text-gray-700">
|
||||
預計到貨日期
|
||||
|
||||
@@ -88,6 +88,10 @@ export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
<span className="text-sm text-gray-500 block mb-1">建立日期</span>
|
||||
<span className="font-medium text-gray-900">{formatDateTime(order.createdAt)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">採購日期</span>
|
||||
<span className="font-medium text-gray-900">{order.orderDate || "-"}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500 block mb-1">預計到貨日期</span>
|
||||
<span className="font-medium text-gray-900">{order.expectedDate || "-"}</span>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type { PurchaseOrder, PurchaseOrderItem, Supplier, PurchaseOrderStatus } from "@/types/purchase-order";
|
||||
import { calculateSubtotal } from "@/utils/purchase-order";
|
||||
import { calculateSubtotal, getTodayDate } from "@/utils/purchase-order";
|
||||
|
||||
interface UsePurchaseOrderFormProps {
|
||||
order?: PurchaseOrder;
|
||||
@@ -14,6 +14,7 @@ interface UsePurchaseOrderFormProps {
|
||||
export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormProps) {
|
||||
const [supplierId, setSupplierId] = useState(order?.supplierId || "");
|
||||
const [expectedDate, setExpectedDate] = useState(order?.expectedDate || "");
|
||||
const [orderDate, setOrderDate] = useState(order?.orderDate || getTodayDate());
|
||||
const [items, setItems] = useState<PurchaseOrderItem[]>(order?.items || []);
|
||||
const [notes, setNotes] = useState(order?.remark || "");
|
||||
const [status, setStatus] = useState<PurchaseOrderStatus>(order?.status || "draft");
|
||||
@@ -32,6 +33,7 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
|
||||
if (order) {
|
||||
setSupplierId(order.supplierId);
|
||||
setExpectedDate(order.expectedDate);
|
||||
setOrderDate(order.orderDate || getTodayDate());
|
||||
setItems(order.items || []);
|
||||
setNotes(order.remark || "");
|
||||
setStatus(order.status);
|
||||
@@ -52,6 +54,7 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
|
||||
const resetForm = () => {
|
||||
setSupplierId("");
|
||||
setExpectedDate("");
|
||||
setOrderDate(getTodayDate());
|
||||
setItems([]);
|
||||
setNotes("");
|
||||
setStatus("draft");
|
||||
@@ -159,6 +162,7 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
|
||||
// State
|
||||
supplierId,
|
||||
expectedDate,
|
||||
orderDate,
|
||||
items,
|
||||
notes,
|
||||
status,
|
||||
@@ -174,6 +178,7 @@ export function usePurchaseOrderForm({ order, suppliers }: UsePurchaseOrderFormP
|
||||
// Setters
|
||||
setSupplierId,
|
||||
setExpectedDate,
|
||||
setOrderDate,
|
||||
setNotes,
|
||||
setStatus,
|
||||
setWarehouseId,
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface PurchaseOrder {
|
||||
poNumber: string;
|
||||
supplierId: string;
|
||||
supplierName: string;
|
||||
orderDate?: string; // 採購日期
|
||||
expectedDate: string;
|
||||
status: PurchaseOrderStatus;
|
||||
items: PurchaseOrderItem[];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 倉庫相關型別定義
|
||||
*/
|
||||
|
||||
export type WarehouseType = "中央倉庫" | "門市";
|
||||
export type WarehouseType = "standard" | "production" | "retail" | "vending" | "transit" | "quarantine";
|
||||
|
||||
/**
|
||||
* 門市資訊
|
||||
@@ -19,17 +19,17 @@ export interface Warehouse {
|
||||
name: string;
|
||||
address?: string;
|
||||
description?: string;
|
||||
createdAt?: string; // 對應 created_at 但前端可能習慣 camelCase,後端傳回 snake_case,Inertia 會保持原樣。
|
||||
// 若後端 Resource 沒轉 camelCase,這裡應該用 snake_case 或在前端轉
|
||||
// 為求簡單,我修改 interface 為 snake_case 以匹配 Laravel 預設 Response
|
||||
createdAt?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
total_quantity?: number;
|
||||
low_stock_count?: number;
|
||||
type?: WarehouseType;
|
||||
is_sellable?: boolean; // 新增欄位
|
||||
book_stock?: number; // 帳面庫存
|
||||
available_stock?: number; // 可用庫存
|
||||
is_sellable?: boolean;
|
||||
license_plate?: string; // 車牌號碼 (移動倉)
|
||||
driver_name?: string; // 司機姓名 (移動倉)
|
||||
book_stock?: number;
|
||||
available_stock?: number;
|
||||
}
|
||||
// 倉庫中的庫存項目
|
||||
export interface WarehouseInventory {
|
||||
@@ -41,6 +41,8 @@ export interface WarehouseInventory {
|
||||
productCode: string;
|
||||
unit: string;
|
||||
quantity: number;
|
||||
unit_cost?: number; // 單位成本
|
||||
total_value?: number; // 總價值
|
||||
safetyStock: number | null;
|
||||
status?: '正常' | '低於'; // 後端可能回傳的狀態
|
||||
batchNumber: string; // 批號 (Mock for now)
|
||||
@@ -56,6 +58,7 @@ export interface GroupedInventory {
|
||||
productCode: string;
|
||||
baseUnit: string;
|
||||
totalQuantity: number;
|
||||
totalValue?: number; // 總價值總計
|
||||
safetyStock: number | null; // 以商品層級顯示的安全庫存
|
||||
status: '正常' | '低於';
|
||||
batches: WarehouseInventory[]; // 該商品下的所有批號庫存
|
||||
@@ -89,6 +92,7 @@ export interface Product {
|
||||
|
||||
export interface WarehouseStats {
|
||||
totalQuantity: number;
|
||||
totalValue?: number; // 倉庫總值
|
||||
lowStockCount: number;
|
||||
replenishmentNeeded: number;
|
||||
}
|
||||
@@ -145,6 +149,7 @@ export interface InventoryTransaction {
|
||||
productName: string;
|
||||
batchNumber: string;
|
||||
quantity: number; // 正數為入庫,負數為出庫
|
||||
unit_cost?: number; // 異動時的成本
|
||||
transactionType: TransactionType;
|
||||
reason?: string;
|
||||
notes?: string;
|
||||
@@ -161,6 +166,7 @@ export interface InboundItem {
|
||||
productId: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unit_cost?: number; // 入庫單價
|
||||
unit: string;
|
||||
baseUnit?: string;
|
||||
largeUnit?: string;
|
||||
|
||||
Reference in New Issue
Block a user