大更新
This commit is contained in:
@@ -244,26 +244,6 @@ export default function ProductDialog({
|
||||
{errors.conversion_rate && <p className="text-sm text-red-500">{errors.conversion_rate}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="purchase_unit_id">採購單位</Label>
|
||||
<Select
|
||||
value={data.purchase_unit_id}
|
||||
onValueChange={(value) => setData("purchase_unit_id", value)}
|
||||
>
|
||||
<SelectTrigger id="purchase_unit_id" className={errors.purchase_unit_id ? "border-red-500" : ""}>
|
||||
<SelectValue placeholder="通常同大單位" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">無</SelectItem>
|
||||
{units.map((unit) => (
|
||||
<SelectItem key={unit.id} value={unit.id.toString()}>
|
||||
{unit.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.purchase_unit_id && <p className="text-sm text-red-500">{errors.purchase_unit_id}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.large_unit_id && data.base_unit_id && data.conversion_rate && (
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order";
|
||||
import { isPriceAlert, formatCurrency } from "@/utils/purchase-order";
|
||||
import { formatCurrency } from "@/utils/purchase-order";
|
||||
|
||||
interface PurchaseOrderItemsTableProps {
|
||||
items: PurchaseOrderItem[];
|
||||
@@ -46,12 +46,13 @@ export function PurchaseOrderItemsTable({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50 hover:bg-gray-50">
|
||||
<TableHead className="w-[25%] text-left">商品名稱</TableHead>
|
||||
<TableHead className="w-[20%] text-left">商品名稱</TableHead>
|
||||
<TableHead className="w-[10%] text-left">數量</TableHead>
|
||||
<TableHead className="w-[10%] text-left">採購單位</TableHead>
|
||||
<TableHead className="w-[15%] text-left">換算基本單位</TableHead>
|
||||
<TableHead className="w-[15%] text-left">預估單價</TableHead>
|
||||
<TableHead className="w-[15%] text-left">小計</TableHead>
|
||||
<TableHead className="w-[12%] text-left">單位</TableHead>
|
||||
<TableHead className="w-[12%] text-left">換算基本單位</TableHead>
|
||||
<TableHead className="w-[12%] text-left">金額</TableHead>
|
||||
<TableHead className="w-[12%] text-left">小計</TableHead>
|
||||
<TableHead className="w-[15%] text-left">單價 / 基本單位</TableHead>
|
||||
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -59,137 +60,184 @@ export function PurchaseOrderItemsTable({
|
||||
{items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={isReadOnly ? 6 : 7}
|
||||
colSpan={isReadOnly ? 7 : 8}
|
||||
className="text-center text-gray-400 py-12 italic"
|
||||
>
|
||||
{isDisabled ? "請先選擇供應商後才能新增商品" : "尚未新增任何商品項"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
{/* 商品選擇 */}
|
||||
<TableCell>
|
||||
{isReadOnly ? (
|
||||
<span className="font-medium">{item.productName}</span>
|
||||
) : (
|
||||
<Select
|
||||
value={item.productId}
|
||||
onValueChange={(value) =>
|
||||
onItemChange?.(index, "productId", value)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger className="h-10 border-gray-200">
|
||||
<SelectValue placeholder="選擇商品" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{supplier?.commonProducts.map((product) => (
|
||||
<SelectItem key={product.productId} value={product.productId}>
|
||||
{product.productName}
|
||||
</SelectItem>
|
||||
))}
|
||||
{(!supplier || supplier.commonProducts.length === 0) && (
|
||||
<div className="p-2 text-sm text-gray-400 text-center">無可用商品</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</TableCell>
|
||||
items.map((item, index) => {
|
||||
// 計算換算後的單價 (基本單位單價)
|
||||
const convertedUnitPrice = item.selectedUnit === 'large' && item.conversion_rate
|
||||
? item.unitPrice / item.conversion_rate
|
||||
: item.unitPrice;
|
||||
|
||||
{/* 數量 */}
|
||||
<TableCell className="text-left">
|
||||
{isReadOnly ? (
|
||||
<span>{Math.floor(item.quantity)}</span>
|
||||
) : (
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
value={item.quantity === 0 ? "" : Math.floor(item.quantity)}
|
||||
onChange={(e) =>
|
||||
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className="h-10 text-left border-gray-200 w-24"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
{/* 商品選擇 */}
|
||||
<TableCell>
|
||||
{isReadOnly ? (
|
||||
<span className="font-medium">{item.productName}</span>
|
||||
) : (
|
||||
<Select
|
||||
value={item.productId}
|
||||
onValueChange={(value) =>
|
||||
onItemChange?.(index, "productId", value)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger className="h-10 border-gray-200">
|
||||
<SelectValue placeholder="選擇商品" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{supplier?.commonProducts.map((product) => (
|
||||
<SelectItem key={product.productId} value={product.productId}>
|
||||
{product.productName}
|
||||
</SelectItem>
|
||||
))}
|
||||
{(!supplier || supplier.commonProducts.length === 0) && (
|
||||
<div className="p-2 text-sm text-gray-400 text-center">無可用商品</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 採購單位 */}
|
||||
<TableCell>
|
||||
<span className="text-gray-500 font-medium">{item.unit || "-"}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* 換算基本單位 */}
|
||||
<TableCell>
|
||||
<span className="text-gray-500 font-medium">
|
||||
{item.conversion_rate && item.base_unit
|
||||
? `${parseFloat((item.quantity * item.conversion_rate).toFixed(2))} ${item.base_unit}`
|
||||
: "-"}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* 單價 */}
|
||||
<TableCell className="text-left">
|
||||
{isReadOnly ? (
|
||||
<span className="font-medium text-gray-900">{formatCurrency(item.unitPrice)}</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{/* 數量 */}
|
||||
<TableCell className="text-left">
|
||||
{isReadOnly ? (
|
||||
<span>{Math.floor(item.quantity)}</span>
|
||||
) : (
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={item.unitPrice || ""}
|
||||
step="1"
|
||||
value={item.quantity === 0 ? "" : Math.floor(item.quantity)}
|
||||
onChange={(e) =>
|
||||
onItemChange?.(index, "unitPrice", Number(e.target.value))
|
||||
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className={`h-10 text-left w-32 ${
|
||||
// 如果有數量但沒有單價,顯示錯誤樣式
|
||||
item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0)
|
||||
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
|
||||
: isPriceAlert(item.unitPrice, item.previousPrice)
|
||||
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
className="h-10 text-left border-gray-200 w-24"
|
||||
/>
|
||||
{/* 錯誤提示:有數量但沒有單價 */}
|
||||
{item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && (
|
||||
<p className="text-[10px] text-red-600 font-medium">
|
||||
❌ 請填寫預估單價
|
||||
</p>
|
||||
)}
|
||||
{/* 價格警示:單價高於上次 */}
|
||||
{item.unitPrice > 0 && isPriceAlert(item.unitPrice, item.previousPrice) && (
|
||||
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
|
||||
⚠️ 高於上次: {formatCurrency(item.previousPrice || 0)}
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 單位選擇 */}
|
||||
<TableCell>
|
||||
{!isReadOnly && item.large_unit_id ? (
|
||||
<Select
|
||||
value={item.selectedUnit || 'base'}
|
||||
onValueChange={(value) =>
|
||||
onItemChange?.(index, "selectedUnit", value)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<SelectTrigger className="h-10 border-gray-200 w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[9999]">
|
||||
<SelectItem value="base">{item.base_unit_name || "個"}</SelectItem>
|
||||
<SelectItem value="large">{item.large_unit_name}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<span className="text-gray-500 font-medium">
|
||||
{item.selectedUnit === 'large' && item.large_unit_name
|
||||
? item.large_unit_name
|
||||
: (item.base_unit_name || "個")}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 換算基本單位 */}
|
||||
<TableCell>
|
||||
<div className="text-gray-700 font-medium">
|
||||
<span>
|
||||
{item.selectedUnit === 'large' && item.conversion_rate
|
||||
? item.quantity * item.conversion_rate
|
||||
: item.quantity}
|
||||
</span>
|
||||
<span className="ml-1 text-gray-500 text-sm">{item.base_unit_name || "個"}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* 單價 */}
|
||||
<TableCell className="text-left">
|
||||
{isReadOnly ? (
|
||||
<span className="font-medium text-gray-900">{formatCurrency(item.unitPrice)}</span>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={item.unitPrice || ""}
|
||||
onChange={(e) =>
|
||||
onItemChange?.(index, "unitPrice", Number(e.target.value))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className={`h-10 text-left w-32 ${
|
||||
// 如果有數量但沒有單價,顯示錯誤樣式
|
||||
item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0)
|
||||
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
/>
|
||||
{/* 錯誤提示 (保留必填提示) */}
|
||||
{item.quantity > 0 && (!item.unitPrice || item.unitPrice <= 0) && (
|
||||
<p className="text-[10px] text-red-600 font-medium">
|
||||
❌ 請填寫金額
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
{/* 小計 */}
|
||||
<TableCell className="text-left">
|
||||
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
|
||||
</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>
|
||||
|
||||
{/* 小計 */}
|
||||
<TableCell className="text-left">
|
||||
<span className="font-bold text-primary">{formatCurrency(item.subtotal)}</span>
|
||||
</TableCell>
|
||||
|
||||
{/* 刪除按鈕 */}
|
||||
{!isReadOnly && onRemoveItem && (
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemoveItem(index)}
|
||||
className="h-8 w-8 text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
{/* 刪除按鈕 */}
|
||||
{!isReadOnly && onRemoveItem && (
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onRemoveItem(index)}
|
||||
className="h-8 w-8 text-gray-300 hover:text-red-500 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -53,8 +53,8 @@ export default function AddSupplyProductDialog({
|
||||
|
||||
// 過濾掉已經在供貨列表中的商品
|
||||
const availableProducts = useMemo(() => {
|
||||
const existingIds = new Set(existingSupplyProducts.map(sp => sp.productId));
|
||||
return products.filter(p => !existingIds.has(p.id));
|
||||
const existingIds = new Set(existingSupplyProducts.map(sp => String(sp.productId)));
|
||||
return products.filter(p => !existingIds.has(String(p.id)));
|
||||
}, [products, existingSupplyProducts]);
|
||||
|
||||
const selectedProduct = availableProducts.find(p => p.id === selectedProductId);
|
||||
@@ -105,7 +105,7 @@ export default function AddSupplyProductDialog({
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[450px] p-0 shadow-lg border-2" align="start">
|
||||
<PopoverContent className="w-[450px] p-0 shadow-lg border-2 z-[9999]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="搜尋商品名稱..." />
|
||||
<CommandList className="max-h-[300px]">
|
||||
@@ -132,7 +132,7 @@ export default function AddSupplyProductDialog({
|
||||
<div className="flex items-center justify-between flex-1">
|
||||
<span className="font-medium">{product.name}</span>
|
||||
<span className="text-xs text-gray-400 bg-gray-50 px-2 py-1 rounded">
|
||||
{product.purchase_unit || product.base_unit || "個"}
|
||||
{product.baseUnit?.name || (product.base_unit as any)?.name || product.base_unit || "個"}
|
||||
</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
@@ -146,15 +146,17 @@ export default function AddSupplyProductDialog({
|
||||
|
||||
{/* 單位(自動帶入) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm font-medium text-gray-500">採購單位</Label>
|
||||
<Label className="text-sm font-medium text-gray-500">單位</Label>
|
||||
<div className="h-10 px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-gray-600 font-medium text-sm flex items-center">
|
||||
{selectedProduct ? (selectedProduct.purchase_unit || selectedProduct.base_unit || "個") : "-"}
|
||||
{selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上次採購價格 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">上次採購單價(選填)</Label>
|
||||
<Label className="text-muted-foreground text-xs">
|
||||
上次採購單價 / {selectedProduct ? (selectedProduct.baseUnit?.name || (selectedProduct.base_unit as any)?.name || selectedProduct.base_unit || "個") : "單位"}
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="輸入價格"
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function EditSupplyProductDialog({
|
||||
|
||||
{/* 上次採購價格 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground text-xs">上次採購單價(選填)</Label>
|
||||
<Label className="text-muted-foreground text-xs">上次採購單價 / {product.baseUnit || "單位"}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="輸入價格"
|
||||
|
||||
@@ -28,15 +28,19 @@ export default function SupplyProductList({
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||
<TableHead className="font-semibold">商品名稱</TableHead>
|
||||
<TableHead className="font-semibold">採購單位</TableHead>
|
||||
<TableHead className="text-right font-semibold">上次採購單價</TableHead>
|
||||
<TableHead className="font-semibold">基本單位</TableHead>
|
||||
<TableHead className="font-semibold">轉換率</TableHead>
|
||||
<TableHead className="text-right font-semibold">
|
||||
上次採購單價
|
||||
<div className="text-xs font-normal text-muted-foreground">(以基本單位計算)</div>
|
||||
</TableHead>
|
||||
<TableHead className="text-center font-semibold w-[150px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{products.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
尚無供貨商品,請點擊上方按鈕新增
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -47,9 +51,26 @@ export default function SupplyProductList({
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell>{product.productName}</TableCell>
|
||||
<TableCell>{product.unit}</TableCell>
|
||||
<TableCell>
|
||||
{product.baseUnit || product.unit || "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{product.largeUnit && product.conversionRate ? (
|
||||
<span className="text-sm text-gray-500">
|
||||
1 {product.largeUnit} = {Number(product.conversionRate)} {product.baseUnit || product.unit}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{product.lastPrice ? `$${product.lastPrice.toLocaleString()}` : "-"}
|
||||
{product.lastPrice ? (
|
||||
<span>
|
||||
${product.lastPrice.toLocaleString()} / {product.baseUnit || product.unit || "單位"}
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getCurrentDateTime, generateOrderNumber } from "@/utils/format";
|
||||
import { getCurrentDateTime } from "@/utils/format";
|
||||
import axios from "axios";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -46,6 +46,7 @@ interface AvailableProduct {
|
||||
productName: string;
|
||||
batchNumber: string;
|
||||
availableQty: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export default function TransferOrderDialog({
|
||||
@@ -276,7 +277,7 @@ export default function TransferOrderDialog({
|
||||
value={`${product.productId}|||${product.batchNumber}`}
|
||||
>
|
||||
{product.productName} (庫存:{" "}
|
||||
{product.availableQty})
|
||||
{product.availableQty} {product.unit})
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
@@ -303,7 +304,7 @@ export default function TransferOrderDialog({
|
||||
<div className="h-5">
|
||||
{selectedProduct && (
|
||||
<p className="text-sm text-gray-500">
|
||||
可用庫存: {selectedProduct.availableQty}
|
||||
可用庫存: {selectedProduct.availableQty} {selectedProduct.unit}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user