UI優化: 全系統狀態標籤 (StatusBadge) 統一化重構完成 (Phase 3 & 4)
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m8s

This commit is contained in:
2026-02-13 13:16:05 +08:00
56 changed files with 3343 additions and 429 deletions

View File

@@ -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

View File

@@ -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">

View File

@@ -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 || '個'}

View File

@@ -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>

View File

@@ -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位置 */}