feat: 統一採購單與操作紀錄 UI、增強各模組操作紀錄功能
- 統一採購單篩選列與表單樣式 (移除舊元件、標準化 Input) - 增強操作紀錄功能 (加入篩選、快照、詳細異動比對) - 統一刪除確認視窗與按鈕樣式 - 修復庫存編輯頁面樣式 - 實作採購單品項異動紀錄 - 實作角色分配異動紀錄 - 擴充供應商與倉庫模組紀錄
This commit is contained in:
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* 日期篩選器元件
|
||||
* 支援快捷日期範圍選項和自定義日期範圍
|
||||
*/
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/**
|
||||
* 採購單篩選器元件
|
||||
*/
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export function PurchaseOrderItemsTable({
|
||||
onItemChange?.(index, "quantity", Math.floor(Number(e.target.value)))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className="h-10 text-left border-gray-200 w-24"
|
||||
className="text-left w-24"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
@@ -161,11 +161,11 @@ export function PurchaseOrderItemsTable({
|
||||
onItemChange?.(index, "subtotal", Number(e.target.value))
|
||||
}
|
||||
disabled={isDisabled}
|
||||
className={`h-10 text-left w-32 ${
|
||||
className={`text-left w-32 ${
|
||||
// 如果有數量但沒有金額,顯示錯誤樣式
|
||||
item.quantity > 0 && (!item.subtotal || item.subtotal <= 0)
|
||||
? "border-red-400 bg-red-50 focus-visible:ring-red-500"
|
||||
: "border-gray-200"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
{/* 錯誤提示 */}
|
||||
|
||||
Reference in New Issue
Block a user