UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { AlertTriangle, Trash2, Eye, ChevronDown, ChevronRight, CheckCircle, Package } from "lucide-react";
|
||||
import { Trash2, Eye, ChevronDown, ChevronRight, Package, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -98,25 +98,22 @@ export default function InventoryTable({
|
||||
|
||||
// 獲取狀態徽章
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case "正常":
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-700 border-green-300">
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
正常
|
||||
</Badge>
|
||||
);
|
||||
|
||||
case "低於":
|
||||
return (
|
||||
<Badge className="bg-red-100 text-red-700 border-red-300">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
低於
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
if (status === '正常') {
|
||||
return (
|
||||
<StatusBadge variant="success">
|
||||
正常
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === '低於') {
|
||||
return (
|
||||
<StatusBadge variant="destructive">
|
||||
低於
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -168,10 +165,9 @@ export default function InventoryTable({
|
||||
{isVending ? '' : (hasInventory ? `${group.batches.length} 個批號` : '無庫存')}
|
||||
</span>
|
||||
{group.batches.some(b => b.expiryDate && new Date(b.expiryDate) < new Date()) && (
|
||||
<Badge className="bg-red-50 text-red-600 border-red-200">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
<StatusBadge variant="destructive">
|
||||
含過期項目
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -199,9 +195,9 @@ export default function InventoryTable({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-gray-500">
|
||||
<StatusBadge variant="neutral">
|
||||
未設定
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
)}
|
||||
{onViewProduct && (
|
||||
<Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Label } from "@/Components/ui/label";
|
||||
import { Checkbox } from "@/Components/ui/checkbox";
|
||||
import { SafetyStockSetting, Product } from "@/types/warehouse";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
|
||||
interface AddSafetyStockDialogProps {
|
||||
open: boolean;
|
||||
@@ -193,7 +193,7 @@ export default function AddSafetyStockDialog({
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{product.name}</div>
|
||||
</div>
|
||||
<Badge variant="outline">{product.type}</Badge>
|
||||
<StatusBadge variant="neutral">{product.type}</StatusBadge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -223,7 +223,7 @@ export default function AddSafetyStockDialog({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{product.name}</span>
|
||||
<Badge variant="outline">{product.type}</Badge>
|
||||
<StatusBadge variant="neutral">{product.type}</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 安全庫存設定列表
|
||||
*/
|
||||
|
||||
import { Trash2, Pencil, CheckCircle, Package, AlertTriangle } from "lucide-react";
|
||||
import { Trash2, Pencil, Package } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { SafetyStockSetting, WarehouseInventory } from "@/types/warehouse";
|
||||
import { calculateProductTotalStock, getSafetyStockStatus } from "@/utils/inventory";
|
||||
import { Can } from "@/Components/Permission/Can";
|
||||
@@ -57,38 +57,35 @@ export default function SafetyStockList({
|
||||
// 如果是自動帶入的品項且尚未存檔,顯示「未設定」
|
||||
if (isNew) {
|
||||
return (
|
||||
<Badge variant="outline" className="text-gray-400 border-gray-200 font-normal">
|
||||
<StatusBadge variant="neutral" className="border-gray-200 font-normal text-gray-400">
|
||||
未設定
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
|
||||
const status = getSafetyStockStatus(quantity, safetyStock);
|
||||
switch (status) {
|
||||
case "正常":
|
||||
return (
|
||||
<Badge className="bg-green-100 text-green-700 border-green-300 hover:bg-green-100">
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
正常
|
||||
</Badge>
|
||||
);
|
||||
case "接近": // 數量 <= 安全庫存 * 1.2
|
||||
return (
|
||||
<Badge className="bg-yellow-100 text-yellow-700 border-yellow-300 hover:bg-yellow-100">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
接近
|
||||
</Badge>
|
||||
);
|
||||
case "低於": // 數量 < 安全庫存
|
||||
return (
|
||||
<Badge className="bg-orange-100 text-orange-700 border-orange-300 hover:bg-orange-100">
|
||||
<AlertTriangle className="mr-1 h-3 w-3" />
|
||||
低於
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
if (status === '正常') {
|
||||
return (
|
||||
<StatusBadge variant="success">
|
||||
正常
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
if (status === '接近') { // 數量 <= 安全庫存 * 1.2
|
||||
return (
|
||||
<StatusBadge variant="warning">
|
||||
接近
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
if (status === '低於') { // 數量 < 安全庫存
|
||||
return (
|
||||
<StatusBadge variant="destructive">
|
||||
低於
|
||||
</StatusBadge>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -118,9 +115,9 @@ export default function SafetyStockList({
|
||||
{setting.productName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="font-normal">
|
||||
<StatusBadge variant="neutral">
|
||||
{setting.productType}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-semibold">
|
||||
{setting.safetyStock} {setting.unit || '個'}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Warehouse, WarehouseStats } from "@/types/warehouse";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Badge } from "@/Components/ui/badge";
|
||||
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||
import { Card, CardContent } from "@/Components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -101,13 +101,12 @@ export default function WarehouseCard({
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Badge
|
||||
variant={warehouse.type === 'quarantine' ? "secondary" : "outline"}
|
||||
className={`text-xs font-normal ${warehouse.type === 'quarantine' ? 'bg-red-100 text-red-700 border-red-200' : ''}`}
|
||||
<StatusBadge
|
||||
variant={warehouse.type === 'quarantine' ? "destructive" : "neutral"}
|
||||
>
|
||||
{WAREHOUSE_TYPE_LABELS[warehouse.type || 'standard'] || '標準倉'}
|
||||
{warehouse.type === 'quarantine' ? ' (不計入可用)' : ' (計入可用)'}
|
||||
</Badge>
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,12 +32,20 @@ import { validateWarehouse } from "@/utils/validation";
|
||||
import { toast } from "sonner";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
|
||||
interface TransitWarehouseOption {
|
||||
id: string;
|
||||
name: string;
|
||||
license_plate?: string;
|
||||
driver_name?: string;
|
||||
}
|
||||
|
||||
interface WarehouseDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
warehouse: Warehouse | null;
|
||||
onSave: (warehouse: Omit<Warehouse, "id" | "createdAt" | "updatedAt">) => void;
|
||||
onDelete?: (warehouseId: string) => void;
|
||||
transitWarehouses?: TransitWarehouseOption[];
|
||||
}
|
||||
|
||||
const WAREHOUSE_TYPE_OPTIONS: { label: string; value: WarehouseType }[] = [
|
||||
@@ -55,6 +63,7 @@ export default function WarehouseDialog({
|
||||
warehouse,
|
||||
onSave,
|
||||
onDelete,
|
||||
transitWarehouses = [],
|
||||
}: WarehouseDialogProps) {
|
||||
const [formData, setFormData] = useState<{
|
||||
code: string;
|
||||
@@ -64,6 +73,7 @@ export default function WarehouseDialog({
|
||||
type: WarehouseType;
|
||||
license_plate: string;
|
||||
driver_name: string;
|
||||
default_transit_warehouse_id: string | null;
|
||||
}>({
|
||||
code: "",
|
||||
name: "",
|
||||
@@ -72,6 +82,7 @@ export default function WarehouseDialog({
|
||||
type: "standard",
|
||||
license_plate: "",
|
||||
driver_name: "",
|
||||
default_transit_warehouse_id: null,
|
||||
});
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
@@ -86,6 +97,7 @@ export default function WarehouseDialog({
|
||||
type: warehouse.type || "standard",
|
||||
license_plate: warehouse.license_plate || "",
|
||||
driver_name: warehouse.driver_name || "",
|
||||
default_transit_warehouse_id: warehouse.default_transit_warehouse_id ? String(warehouse.default_transit_warehouse_id) : null,
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
@@ -96,6 +108,7 @@ export default function WarehouseDialog({
|
||||
type: "standard",
|
||||
license_plate: "",
|
||||
driver_name: "",
|
||||
default_transit_warehouse_id: null,
|
||||
});
|
||||
}
|
||||
}, [warehouse, open]);
|
||||
@@ -216,6 +229,32 @@ export default function WarehouseDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 預設在途倉設定(僅非 transit 類型顯示) */}
|
||||
{formData.type !== 'transit' && transitWarehouses.length > 0 && (
|
||||
<div className="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||
<div className="border-b border-blue-200 pb-2">
|
||||
<h4 className="text-sm text-blue-800 font-medium">調撥配送設定</h4>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>預設在途倉</Label>
|
||||
<p className="text-xs text-gray-500">從此倉庫建立調撥單時,系統將自動帶入此在途倉作為配送中繼倉</p>
|
||||
<SearchableSelect
|
||||
value={formData.default_transit_warehouse_id || ""}
|
||||
onValueChange={(val) => setFormData({ ...formData, default_transit_warehouse_id: val || null })}
|
||||
options={[
|
||||
{ label: "不指定", value: "" },
|
||||
...transitWarehouses.map((tw) => ({
|
||||
label: `${tw.name}${tw.license_plate ? ` (${tw.license_plate})` : ''}`,
|
||||
value: tw.id,
|
||||
})),
|
||||
]}
|
||||
placeholder="選擇預設在途倉"
|
||||
className="h-9 bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{/* 區塊 B:位置 */}
|
||||
|
||||
Reference in New Issue
Block a user