feat: 標準化全系統數值輸入欄位與擴充商品價格功能
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m0s

1. UI 標準化:
   - 針對全系統數值輸入欄位統一加上 step='any' 以支援小數點。
   - 表格形式 (Table) 的數值輸入欄位統一加上 text-right 靠右對齊。
   - 修正 Components 與 Pages 中所有涉及金額與數量的輸入框。

2. 功能擴充與修正:
   - 擴充 Product 模型與相關 Dialog 以支援多種價格設定。
   - 修正 Inventory/GoodsReceipt/Create.tsx 未使用的變數錯誤。
   - 優化庫存相關頁面的 UI 一致性。

3. 其他:
   - 更新相關的 Type 定義與 Controller 邏輯。
This commit is contained in:
2026-02-05 11:45:08 +08:00
parent 04f3891275
commit 3ce96537b3
40 changed files with 774 additions and 212 deletions

View File

@@ -48,8 +48,10 @@ interface AdjItem {
qty_before: number | string;
adjust_qty: number | string;
notes: string;
expiry_date?: string | null;
}
interface AdjDoc {
id: string;
doc_no: string;
@@ -155,6 +157,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
qty_before: inv.quantity || 0,
adjust_qty: 0,
notes: '',
expiry_date: inv.expiry_date,
});
addedCount++;
}
@@ -409,9 +412,10 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
onCheckedChange={() => toggleSelectAll()}
/>
</TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="text-right font-medium text-grey-600 pr-6"></TableHead>
</TableRow>
</TableHeader>
@@ -447,9 +451,10 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
onCheckedChange={() => toggleSelect(key)}
/>
</TableCell>
<TableCell className="font-mono text-sm text-grey-1">{inv.product_code}</TableCell>
<TableCell className="font-semibold text-grey-0">{inv.product_name}</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.batch_number || '-'}</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.expiry_date || '-'}</TableCell>
<TableCell className="text-right font-bold text-primary-main pr-6">{inv.quantity} {inv.unit_name}</TableCell>
</TableRow>
);
@@ -532,7 +537,14 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
<span className="text-xs text-gray-500 font-mono">{item.product_code}</span>
</div>
</TableCell>
<TableCell className="text-grey-600 font-mono text-sm">{item.batch_number || '-'}</TableCell>
<TableCell className="text-grey-600 font-mono text-sm">
<div>{item.batch_number || '-'}</div>
{item.expiry_date && (
<div className="text-xs text-gray-400 mt-1">
: {item.expiry_date}
</div>
)}
</TableCell>
<TableCell className="text-center text-grey-500">{item.unit}</TableCell>
<TableCell className="text-right font-medium text-grey-400">
{item.qty_before}
@@ -542,8 +554,8 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
<div className="flex justify-end pr-2">
<Input
type="number"
step="0.01"
className="text-right h-9 w-32 font-medium"
step="any"
className="h-9 w-32 font-medium text-right"
value={item.adjust_qty}
onChange={e => updateItem(index, 'adjust_qty', e.target.value)}
/>
@@ -569,9 +581,9 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
{!isReadOnly && !doc.count_doc_id && (
<TableCell className="text-center">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 text-red-400 hover:text-red-600 hover:bg-red-50 p-0"
variant="outline"
size="icon"
className="button-outlined-error h-8 w-8"
onClick={() => removeItem(index)}
>
<Trash2 className="h-4 w-4" />

View File

@@ -276,7 +276,14 @@ export default function Show({ doc }: any) {
<span className="text-xs text-gray-500 font-mono">{item.product_code}</span>
</div>
</TableCell>
<TableCell className="text-sm font-mono">{item.batch_number || '-'}</TableCell>
<TableCell className="text-sm font-mono">
<div>{item.batch_number || '-'}</div>
{item.expiry_date && (
<div className="text-xs text-gray-400 mt-1">
: {item.expiry_date}
</div>
)}
</TableCell>
<TableCell className="text-right font-medium">{Number(item.system_qty)}</TableCell>
<TableCell className="text-right px-1 py-3">
{isReadOnly ? (
@@ -284,12 +291,12 @@ export default function Show({ doc }: any) {
) : (
<Input
type="number"
step="0.01"
step="any"
value={formItem.counted_qty ?? ''}
onChange={(e) => updateItem(index, 'counted_qty', e.target.value)}
onWheel={(e: any) => e.target.blur()}
disabled={processing}
className="h-9 text-right font-medium focus:ring-primary-main"
className="h-9 font-medium focus:ring-primary-main text-right"
placeholder="盤點..."
/>
)}

View File

@@ -38,13 +38,7 @@ import { STATUS_CONFIG } from '@/constants/purchase-order';
interface BatchItem {
inventoryId: string;
batchNumber: string;
originCountry: string;
expiryDate: string | null;
quantity: number;
}
// 待進貨採購單 Item 介面
interface PendingPOItem {
@@ -207,13 +201,12 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
};
// Batch management
const [batchesCache, setBatchesCache] = useState<Record<string, BatchItem[]>>({});
const [nextSequences, setNextSequences] = useState<Record<string, number>>({});
// Fetch batches and sequence for a product
const fetchProductBatches = async (productId: number, country: string = 'TW', dateStr: string = '') => {
if (!data.warehouse_id) return;
const cacheKey = `${productId}-${data.warehouse_id}`;
// const cacheKey = `${productId}-${data.warehouse_id}`; // Unused
try {
const today = new Date().toISOString().split('T')[0];
@@ -233,13 +226,7 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
);
if (response.data) {
// Update existing batches list
if (response.data.batches) {
setBatchesCache(prev => ({
...prev,
[cacheKey]: response.data.batches
}));
}
// Remove unused batch cache update
// Update next sequence for new batch generation
if (response.data.nextSequence !== undefined) {
@@ -645,11 +632,11 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
<TableCell>
<Input
type="number"
step="1"
step="any"
min="0"
value={item.quantity_received}
onChange={(e) => updateItem(index, 'quantity_received', e.target.value)}
className={`w-full ${(errors as any)[errorKey] ? 'border-red-500' : ''}`}
className={`w-full text-right ${errors && (errors as any)[errorKey] ? 'border-red-500' : ''}`}
/>
{(errors as any)[errorKey] && (
<p className="text-xs text-red-500 mt-1">{(errors as any)[errorKey]}</p>

View File

@@ -116,6 +116,7 @@ export default function Show({ order }: any) {
product_name: inv.product_name,
product_code: inv.product_code,
batch_number: inv.batch_number,
expiry_date: inv.expiry_date,
unit: inv.unit_name,
quantity: 1, // Default 1
max_quantity: inv.quantity, // Max available
@@ -371,9 +372,10 @@ export default function Show({ order }: any) {
onCheckedChange={() => toggleSelectAll()}
/>
</TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="font-medium text-grey-600"></TableHead>
<TableHead className="text-right font-medium text-grey-600 pr-6"></TableHead>
</TableRow>
</TableHeader>
@@ -409,9 +411,10 @@ export default function Show({ order }: any) {
onCheckedChange={() => toggleSelect(key)}
/>
</TableCell>
<TableCell className="font-mono text-sm text-grey-1">{inv.product_code}</TableCell>
<TableCell className="font-semibold text-grey-0">{inv.product_name}</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.batch_number || '-'}</TableCell>
<TableCell className="text-sm font-mono text-grey-2">{inv.expiry_date || '-'}</TableCell>
<TableCell className="text-right font-bold text-primary-main pr-6">{inv.quantity} {inv.unit_name}</TableCell>
</TableRow>
);
@@ -493,7 +496,14 @@ export default function Show({ order }: any) {
<span className="text-xs text-gray-500 font-mono">{item.product_code}</span>
</div>
</TableCell>
<TableCell className="text-sm font-mono">{item.batch_number || '-'}</TableCell>
<TableCell className="text-sm font-mono">
<div>{item.batch_number || '-'}</div>
{item.expiry_date && (
<div className="text-xs text-gray-400 mt-1">
: {item.expiry_date}
</div>
)}
</TableCell>
<TableCell className="text-right font-semibold text-primary-main">
{item.max_quantity} {item.unit || item.unit_name}
</TableCell>
@@ -505,10 +515,10 @@ export default function Show({ order }: any) {
<Input
type="number"
min="0.01"
step="0.01"
step="any"
value={item.quantity}
onChange={(e) => handleUpdateItem(index, 'quantity', e.target.value)}
className="h-9 w-32 text-right font-medium focus:ring-primary-main"
className="h-9 w-32 font-medium focus:ring-primary-main text-right"
/>
</div>
)}
@@ -528,7 +538,7 @@ export default function Show({ order }: any) {
</TableCell>
{!isReadOnly && (
<TableCell className="text-center">
<Button variant="ghost" size="sm" className="h-8 w-8 text-red-400 hover:text-red-600 hover:bg-red-50 p-0" onClick={() => handleRemoveItem(index)}>
<Button variant="outline" size="icon" className="button-outlined-error h-8 w-8" onClick={() => handleRemoveItem(index)}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>