first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
/**
* 日期篩選器元件
* 支援快捷日期範圍選項和自定義日期範圍
*/
import { Calendar } from "lucide-react";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Button } from "@/Components/ui/button";
export interface DateRange {
start: string; // YYYY-MM-DD 格式
end: string; // YYYY-MM-DD 格式
}
interface DateFilterProps {
dateRange: DateRange | null;
onDateRangeChange: (range: DateRange | null) => void;
}
// 格式化日期為 YYYY-MM-DD
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
// 獲取從今天往前 N 天的日期範圍
function getDateRange(days: number): DateRange {
const end = new Date();
const start = new Date();
start.setDate(start.getDate() - days);
return {
start: formatDate(start),
end: formatDate(end),
};
}
// 獲取本月的日期範圍
function getCurrentMonth(): DateRange {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
return {
start: formatDate(start),
end: formatDate(end),
};
}
// 獲取上月的日期範圍
function getLastMonth(): DateRange {
const now = new Date();
const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const end = new Date(now.getFullYear(), now.getMonth(), 0);
return {
start: formatDate(start),
end: formatDate(end),
};
}
// 快捷日期選項
const DATE_SHORTCUTS = [
{ label: "今天", getValue: () => getDateRange(0) },
{ label: "最近7天", getValue: () => getDateRange(7) },
{ label: "最近30天", getValue: () => getDateRange(30) },
{ label: "本月", getValue: () => getCurrentMonth() },
{ label: "上月", getValue: () => getLastMonth() },
];
export function DateFilter({ dateRange, onDateRangeChange }: DateFilterProps) {
const handleStartDateChange = (value: string) => {
if (!value) {
onDateRangeChange(null);
return;
}
onDateRangeChange({
start: value,
end: dateRange?.end || value,
});
};
const handleEndDateChange = (value: string) => {
if (!value) {
onDateRangeChange(null);
return;
}
onDateRangeChange({
start: dateRange?.start || value,
end: value,
});
};
const handleShortcutClick = (getValue: () => DateRange) => {
onDateRangeChange(getValue());
};
const handleClearClick = () => {
onDateRangeChange(null);
};
return (
<div className="space-y-4">
{/* 標題 */}
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-500" />
<Label className="font-semibold text-gray-700"></Label>
</div>
{/* 快捷選項 */}
<div className="flex flex-wrap gap-2">
{DATE_SHORTCUTS.map((shortcut) => (
<Button
key={shortcut.label}
variant="outline"
size="sm"
onClick={() => handleShortcutClick(shortcut.getValue)}
className="button-outlined-primary border-gray-200"
>
{shortcut.label}
</Button>
))}
</div>
{/* 自定義日期範圍 */}
<div className="space-y-4 bg-gray-50/50 p-4 rounded-lg border border-dashed border-gray-200">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* 開始日期 */}
<div className="space-y-2">
<Label htmlFor="start-date" className="text-sm text-gray-500">
</Label>
<Input
id="start-date"
type="date"
value={dateRange?.start || ""}
onChange={(e) => handleStartDateChange(e.target.value)}
className="border-gray-200 focus:border-primary"
/>
</div>
{/* 結束日期 */}
<div className="space-y-2">
<Label htmlFor="end-date" className="text-sm text-gray-500">
</Label>
<Input
id="end-date"
type="date"
value={dateRange?.end || ""}
onChange={(e) => handleEndDateChange(e.target.value)}
className="border-gray-200 focus:border-primary"
/>
</div>
</div>
{/* 清除按鈕 */}
{dateRange && (
<Button
variant="outline"
size="sm"
onClick={handleClearClick}
className="w-full button-outlined-primary border-gray-200"
>
</Button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { Edit, Eye } from "lucide-react";
import { Button } from "@/Components/ui/button";
import { Link } from "@inertiajs/react";
import type { PurchaseOrder } from "@/types/purchase-order";
export function PurchaseOrderActions({
order,
}: { order: PurchaseOrder }) {
return (
<div className="flex justify-end gap-2">
<Link href={`/purchase-orders/${order.id}`}>
<Button
variant="outline"
size="sm"
className="button-outlined-primary h-8 w-8 p-0"
title="查看採購單"
>
<Eye className="h-4 w-4" />
</Button>
</Link>
<Link href={`/purchase-orders/${order.id}/edit`}>
<Button
variant="outline"
size="sm"
className="button-outlined-primary h-8 w-8 p-0"
title="編輯採購單"
>
<Edit className="h-4 w-4" />
</Button>
</Link>
</div>
);
}

View File

@@ -0,0 +1,135 @@
/**
* 採購單篩選器元件
*/
import { useState } from "react";
import { Search, X, Filter, ChevronDown, ChevronUp } from "lucide-react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { DateFilter, type DateRange } from "./DateFilter";
interface PurchaseOrderFiltersProps {
searchQuery: string;
statusFilter: string;
requesterFilter: string;
warehouses: { id: string | number; name: string }[];
dateRange: DateRange | null;
onSearchChange: (value: string) => void;
onStatusChange: (value: string) => void;
onRequesterChange: (value: string) => void;
onDateRangeChange: (range: DateRange | null) => void;
onClearFilters: () => void;
hasActiveFilters: boolean;
}
export function PurchaseOrderFilters({
searchQuery,
statusFilter,
requesterFilter,
warehouses,
dateRange,
onSearchChange,
onStatusChange,
onRequesterChange,
onDateRangeChange,
onClearFilters,
hasActiveFilters,
}: PurchaseOrderFiltersProps) {
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
return (
<div className="bg-white rounded-lg border shadow-sm p-4 space-y-4">
{/* 主要篩選列 */}
<div className="flex flex-col lg:flex-row gap-4">
{/* 搜尋框 */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
placeholder="搜尋採購單編號"
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="h-10 pl-10 border-gray-200 focus:border-primary"
/>
</div>
{/* 快速篩選區 */}
<div className="flex flex-wrap gap-3 items-center">
{/* 狀態篩選 */}
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-gray-400 hidden sm:block" />
<Select value={statusFilter} onValueChange={onStatusChange}>
<SelectTrigger className="w-[160px] h-10 border-gray-200">
<SelectValue placeholder="全部狀態" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="processing"></SelectItem>
<SelectItem value="shipping"></SelectItem>
<SelectItem value="confirming"></SelectItem>
<SelectItem value="completed"></SelectItem>
<SelectItem value="cancelled"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 倉庫篩選 */}
<Select value={requesterFilter} onValueChange={onRequesterChange}>
<SelectTrigger className="w-[180px] h-10 border-gray-200">
<SelectValue placeholder="全部倉庫" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{warehouses.map((warehouse) => (
<SelectItem key={warehouse.id} value={String(warehouse.id)}>
{warehouse.name}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 進階篩選按鈕 */}
<Button
variant="outline"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="gap-2 button-outlined-primary h-10 border-gray-200"
>
{showAdvancedFilters ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="hidden sm:inline"></span>
</Button>
{/* 清除篩選按鈕 */}
{hasActiveFilters && (
<Button
variant="outline"
onClick={onClearFilters}
className="gap-2 button-outlined-primary h-10 border-gray-200"
>
<X className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button>
)}
</div>
</div>
{/* 進階篩選區 */}
{showAdvancedFilters && (
<div className="pt-4 border-t border-gray-100">
<DateFilter dateRange={dateRange} onDateRangeChange={onDateRangeChange} />
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,177 @@
/**
* 採購單商品表格元件
*/
import { Trash2 } from "lucide-react";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import type { PurchaseOrderItem, Supplier } from "@/types/purchase-order";
import { isPriceAlert, formatCurrency } from "@/utils/purchase-order";
interface PurchaseOrderItemsTableProps {
items: PurchaseOrderItem[];
supplier?: Supplier;
isReadOnly?: boolean;
isDisabled?: boolean;
onAddItem?: () => void;
onRemoveItem?: (index: number) => void;
onItemChange?: (index: number, field: keyof PurchaseOrderItem, value: string | number) => void;
}
export function PurchaseOrderItemsTable({
items,
supplier,
isReadOnly = false,
isDisabled = false,
onRemoveItem,
onItemChange,
}: PurchaseOrderItemsTableProps) {
return (
<div className={`border rounded-lg overflow-hidden ${isDisabled ? "opacity-50 pointer-events-none grayscale" : ""}`}>
<Table>
<TableHeader>
<TableRow className="bg-gray-50 hover:bg-gray-50">
<TableHead className="w-[30%] text-left"></TableHead>
<TableHead className="w-[15%] text-left"></TableHead>
<TableHead className="w-[10%] text-left"></TableHead>
<TableHead className="w-[20%] text-left"></TableHead>
<TableHead className="w-[20%] text-left"></TableHead>
{!isReadOnly && <TableHead className="w-[5%]"></TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell
colSpan={isReadOnly ? 5 : 6}
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>
{/* 數量 */}
<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>
{/* 單位 */}
<TableCell>
<span className="text-gray-500 font-medium">{item.unit || "-"}</span>
</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 ${isPriceAlert(item.unitPrice, item.previousPrice)
? "border-amber-400 bg-amber-50 focus-visible:ring-amber-500"
: "border-gray-200"
}`}
/>
{isPriceAlert(item.unitPrice, item.previousPrice) && (
<p className="text-[10px] text-amber-600 font-medium animate-pulse">
: {formatCurrency(item.previousPrice || 0)}
</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>
))
)}
</TableBody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,48 @@
/**
* 採購單狀態標籤組件
*/
import { Badge } from "@/Components/ui/badge";
import { PurchaseOrderStatus } from "@/types/purchase-order";
interface PurchaseOrderStatusBadgeProps {
status: PurchaseOrderStatus;
className?: string;
}
export default function PurchaseOrderStatusBadge({
status,
className,
}: PurchaseOrderStatusBadgeProps) {
const getStatusConfig = (status: PurchaseOrderStatus) => {
switch (status) {
case "draft":
return { label: "草稿", className: "bg-gray-100 text-gray-700 border-gray-200" };
case "pending":
return { label: "待審核", className: "bg-blue-100 text-blue-700 border-blue-200" };
case "processing":
return { label: "處理中", className: "bg-yellow-100 text-yellow-700 border-yellow-200" };
case "shipping":
return { label: "運送中", className: "bg-purple-100 text-purple-700 border-purple-200" };
case "confirming":
return { label: "待確認", className: "bg-orange-100 text-orange-700 border-orange-200" };
case "completed":
return { label: "已完成", className: "bg-green-100 text-green-700 border-green-200" };
case "cancelled":
return { label: "已取消", className: "bg-red-100 text-red-700 border-red-200" };
default:
return { label: "未知", className: "bg-gray-100 text-gray-700 border-gray-200" };
}
};
const config = getStatusConfig(status);
return (
<Badge
variant="outline"
className={`${config.className} ${className} font-medium px-2.5 py-0.5 rounded-full`}
>
{config.label}
</Badge>
);
}

View File

@@ -0,0 +1,230 @@
/**
* 採購單列表表格
*/
import { useState, useMemo } from "react";
import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { PurchaseOrderActions } from "./PurchaseOrderActions";
import PurchaseOrderStatusBadge from "./PurchaseOrderStatusBadge";
import CopyButton from "@/Components/shared/CopyButton";
import type { PurchaseOrder } from "@/types/purchase-order";
import { formatCurrency, formatDateTime } from "@/utils/format";
import { STATUS_CONFIG } from "@/constants/purchase-order";
interface PurchaseOrderTableProps {
orders: PurchaseOrder[];
}
type SortField = "poNumber" | "warehouse_name" | "supplierName" | "createdAt" | "totalAmount" | "status";
type SortDirection = "asc" | "desc" | null;
export default function PurchaseOrderTable({
orders,
}: PurchaseOrderTableProps) {
const [sortField, setSortField] = useState<SortField | null>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
// 處理排序
const handleSort = (field: SortField) => {
if (sortField === field) {
if (sortDirection === "asc") {
setSortDirection("desc");
} else if (sortDirection === "desc") {
setSortDirection(null);
setSortField(null);
} else {
setSortDirection("asc");
}
} else {
setSortField(field);
setSortDirection("asc");
}
};
// 排序後的訂單列表
const sortedOrders = useMemo(() => {
if (!sortField || !sortDirection) {
return orders;
}
return [...orders].sort((a, b) => {
let aValue: string | number;
let bValue: string | number;
switch (sortField) {
case "poNumber":
aValue = a.poNumber;
bValue = b.poNumber;
break;
case "warehouse_name":
aValue = a.warehouse_name || "";
bValue = b.warehouse_name || "";
break;
case "supplierName":
aValue = a.supplierName;
bValue = b.supplierName;
break;
case "createdAt":
aValue = a.createdAt;
bValue = b.createdAt;
break;
case "totalAmount":
aValue = a.totalAmount;
bValue = b.totalAmount;
break;
case "status":
aValue = STATUS_CONFIG[a.status].label;
bValue = STATUS_CONFIG[b.status].label;
break;
default:
return 0;
}
if (typeof aValue === "string" && typeof bValue === "string") {
return sortDirection === "asc"
? aValue.localeCompare(bValue, "zh-TW")
: bValue.localeCompare(aValue, "zh-TW");
} else {
return sortDirection === "asc"
? (aValue as number) - (bValue as number)
: (bValue as number) - (aValue as number);
}
});
}, [orders, sortField, sortDirection]);
const SortIcon = ({ field }: { field: SortField }) => {
if (sortField !== field) {
return <ArrowUpDown className="h-4 w-4 text-muted-foreground" />;
}
if (sortDirection === "asc") {
return <ArrowUp className="h-4 w-4 text-primary" />;
}
if (sortDirection === "desc") {
return <ArrowDown className="h-4 w-4 text-primary" />;
}
return <ArrowUpDown className="h-4 w-4 text-muted-foreground" />;
};
return (
<div className="bg-white rounded-lg border shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-gray-50/50">
<TableHead className="w-[50px]">#</TableHead>
<TableHead className="w-[180px]">
<button
onClick={() => handleSort("poNumber")}
className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="poNumber" />
</button>
</TableHead>
<TableHead className="w-[200px]">
<button
onClick={() => handleSort("warehouse_name")}
className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="warehouse_name" />
</button>
</TableHead>
<TableHead className="w-[180px]">
<button
onClick={() => handleSort("supplierName")}
className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="supplierName" />
</button>
</TableHead>
<TableHead className="w-[150px]">
<button
onClick={() => handleSort("createdAt")}
className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="createdAt" />
</button>
</TableHead>
<TableHead className="w-[140px] text-right">
<button
onClick={() => handleSort("totalAmount")}
className="flex items-center gap-2 ml-auto hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="totalAmount" />
</button>
</TableHead>
<TableHead className="w-[120px]">
<button
onClick={() => handleSort("status")}
className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
>
<SortIcon field="status" />
</button>
</TableHead>
<TableHead className="text-right font-semibold"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedOrders.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center text-muted-foreground py-12">
</TableCell>
</TableRow>
) : (
sortedOrders.map((order, index) => (
<TableRow key={order.id}>
<TableCell className="text-gray-500 font-medium text-center">
{index + 1}
</TableCell>
<TableCell>
<div className="flex items-center gap-1.5">
<span className="font-mono text-sm font-medium">{order.poNumber}</span>
<CopyButton text={order.poNumber} label="複製單號" />
</div>
</TableCell>
<TableCell>
<div className="space-y-0.5">
<div className="text-sm font-medium text-gray-900">{order.warehouse_name}</div>
<div className="text-xs text-gray-500">{order.createdBy}</div>
</div>
</TableCell>
<TableCell>
<span className="text-sm text-gray-700">{order.supplierName}</span>
</TableCell>
<TableCell>
<span className="text-sm text-gray-500">{formatDateTime(order.createdAt)}</span>
</TableCell>
<TableCell className="text-right">
<span className="font-semibold text-gray-900">{formatCurrency(order.totalAmount)}</span>
</TableCell>
<TableCell>
<PurchaseOrderStatusBadge status={order.status} />
</TableCell>
<TableCell>
<PurchaseOrderActions
order={order}
/>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,95 @@
/**
* 狀態流程條組件
*/
import { Check } from "lucide-react";
import type { PurchaseOrderStatus } from "@/types/purchase-order";
interface StatusProgressBarProps {
currentStatus: PurchaseOrderStatus;
}
// 流程步驟定義
const FLOW_STEPS: { key: PurchaseOrderStatus | "approved"; label: string }[] = [
{ key: "draft", label: "草稿" },
{ key: "pending", label: "待審核" },
{ key: "processing", label: "處理中" },
{ key: "shipping", label: "運送中" },
{ key: "confirming", label: "待確認" },
{ key: "completed", label: "已完成" },
];
export function StatusProgressBar({ currentStatus }: StatusProgressBarProps) {
// 對於 cancelled 狀態,進度條通常不顯示或顯示特殊樣式,這裡我們顯示到最後一個有效狀態
const effectiveStatus = currentStatus === "cancelled" ? "pending" : currentStatus;
// 找到當前狀態在流程中的位置
const currentIndex = FLOW_STEPS.findIndex((step) => step.key === effectiveStatus);
return (
<div className="bg-white rounded-lg border shadow-sm p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-6"></h3>
<div className="relative px-4">
{/* 進度條背景 */}
<div className="absolute top-5 left-8 right-8 h-0.5 bg-gray-100" />
{/* 進度條進度 */}
{currentIndex >= 0 && (
<div
className="absolute top-5 left-8 h-0.5 bg-primary transition-all duration-500"
style={{
width: `${(currentIndex / (FLOW_STEPS.length - 1)) * 100}%`,
maxWidth: "calc(100% - 4rem)"
}}
/>
)}
{/* 步驟標記 */}
<div className="relative flex justify-between">
{FLOW_STEPS.map((step, index) => {
const isCompleted = index < currentIndex;
const isCurrent = index === currentIndex;
// 如果當前是 cancelled且我們正在渲染 pending 步驟,可以加點提示
const isRejectedAtThisStep = currentStatus === "cancelled" && step.key === "pending";
return (
<div key={step.key} className="flex flex-col items-center flex-1">
{/* 圓點 */}
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 transition-all duration-300 ${isRejectedAtThisStep
? "bg-red-500 border-red-500 text-white"
: isCompleted
? "bg-primary border-primary text-white"
: isCurrent
? "bg-white border-primary text-primary ring-4 ring-primary/10 font-bold"
: "bg-white border-gray-200 text-gray-400"
}`}
>
{isCompleted && !isRejectedAtThisStep ? (
<Check className="h-5 w-5" />
) : (
<span className="text-sm">{index + 1}</span>
)}
</div>
{/* 標籤 */}
<div className="mt-3 text-center">
<p
className={`text-xs whitespace-nowrap transition-colors ${isRejectedAtThisStep
? "text-red-600 font-bold"
: isCompleted || isCurrent
? "text-gray-900 font-bold"
: "text-gray-400"
}`}
>
{isRejectedAtThisStep ? "已取消" : step.label}
</p>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}