diff --git a/.agent/skills/ui-consistency/SKILL.md b/.agent/skills/ui-consistency/SKILL.md index be2803d..b09b6d0 100644 --- a/.agent/skills/ui-consistency/SKILL.md +++ b/.agent/skills/ui-consistency/SKILL.md @@ -775,6 +775,38 @@ import { Input } from "@/Components/ui/input"; /> ``` +## 11.8 篩選列規範 (Filter Bar Norms) + +列表頁面的篩選區域(Filter Bar)應遵循以下規範以節省空間並保持層級清晰: + +1. **標籤文字 (Labels)**: 使用 **`text-xs`** (`12px`) 大小,顏色建議使用 `text-gray-500` 或 `text-grey-2`。這與一般表單 (`text-sm`) 不同,目的是降低篩選列的視覺權重。 +2. **輸入元件高度**: 統一使用 **`h-9`** (`36px`)。 +3. **佈局**: + - **容器內距**: 統一使用 **`p-5`** (`20px`)。 + - **Grid 間距**: 建議使用 **`gap-4`** (`16px`) 或 `gap-6` (`24px`),但同一專案內需統一。本專案推薦 **`gap-4`**。 + - **垂直間距**: Label 與 Input 之間使用 **`space-y-1`** (`4px`)。 + - **排版**: 建議使用 Grid 系統 (`grid-cols-12`) 進行排版。 + +```tsx +
+ + +
+``` + +4. **操作按鈕區 (Action Bar)**: + - **位置**: 位於篩選列最下方。 + - **樣式**: 統一使用 `flex items-center justify-end border-t border-grey-4 pt-5 gap-3`。 + - **說明**: `border-grey-4` 為標準通用邊框色,`pt-5` 與容器 padding (`p-5`) 呼應,維持視覺平衡。 + +5. **收合模式 (Collapsible Mode)**: + - **目的**: 節省垂直空間,預設隱藏較佔空間與低頻使用的篩選器(如日期區間)。 + - **實作**: + - 預設狀態:若無相關篩選值,則預設為 **收合 (Collapsed)**。 + - 切換按鈕:位於 Action Bar 左側 (`mr-auto`)。 + - 樣式:Ghost Button + `ChevronDown`/`ChevronUp` Icon + 提示圓點 (Indicator)。 + - **邏輯**: 若載入頁面時已有被隱藏的篩選值 (e.g. `date_start`),則強制 **展開 (Expanded)** 或顯示提示。 + --- ## 12. 檢查清單 diff --git a/app/Http/Controllers/UtilityFeeController.php b/app/Http/Controllers/UtilityFeeController.php index e67ec25..7fdfbec 100644 --- a/app/Http/Controllers/UtilityFeeController.php +++ b/app/Http/Controllers/UtilityFeeController.php @@ -42,7 +42,7 @@ class UtilityFeeController extends Controller if ($sortField && $sortDirection) { $query->orderBy($sortField, $sortDirection); } else { - $query->orderBy('transaction_date', 'desc'); + $query->orderBy('created_at', 'desc'); } $fees = $query->paginate($request->input('per_page', 15))->withQueryString(); diff --git a/resources/js/Pages/Admin/ActivityLog/Index.tsx b/resources/js/Pages/Admin/ActivityLog/Index.tsx index fd548cb..1449fac 100644 --- a/resources/js/Pages/Admin/ActivityLog/Index.tsx +++ b/resources/js/Pages/Admin/ActivityLog/Index.tsx @@ -4,13 +4,14 @@ import { Head, router } from '@inertiajs/react'; import { PageProps } from '@/types/global'; import Pagination from '@/Components/shared/Pagination'; import { SearchableSelect } from "@/Components/ui/searchable-select"; -import { FileText, Search, RotateCcw, Calendar } from 'lucide-react'; +import { FileText, Search, RotateCcw, Calendar, ChevronDown, ChevronUp } from 'lucide-react'; import LogTable, { Activity } from '@/Components/ActivityLog/LogTable'; import ActivityDetailDialog from '@/Components/ActivityLog/ActivityDetailDialog'; import { Button } from '@/Components/ui/button'; import { Input } from '@/Components/ui/input'; import { Label } from '@/Components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select"; +import { getDateRange } from "@/utils/format"; interface PaginationLinks { url: string | null; @@ -54,6 +55,21 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u const [event, setEvent] = useState(filters.event || 'all'); const [subjectType, setSubjectType] = useState(filters.subject_type || 'all'); const [causer, setCauser] = useState(filters.causer_id || 'all'); + const [dateRangeType, setDateRangeType] = useState('custom'); + + // Advanced Filter Toggle + const [showAdvancedFilter, setShowAdvancedFilter] = useState( + !!(filters.date_start || filters.date_end) + ); + + const handleDateRangeChange = (type: string) => { + setDateRangeType(type); + if (type === 'custom') return; + + const { start, end } = getDateRange(type); + setDateStart(start); + setDateEnd(end); + }; const handleFilter = () => { router.get( @@ -79,6 +95,7 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u setEvent('all'); setSubjectType('all'); setCauser('all'); + setDateRangeType('custom'); router.get( route('activity-logs.index'), @@ -144,44 +161,29 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u {/* 篩選區塊 */} -
-
+
+
{/* 關鍵字搜尋 */} -
- +
+
- + setSearch(e.target.value)} - className="pl-9" + className="pl-10 h-9 block" onKeyDown={(e) => e.key === 'Enter' && handleFilter()} />
- {/* 操作人員 */} -
- - -
- {/* 事件類型 */} -
- +
+ setDateStart(e.target.value)} - className="pl-9 block w-full" - /> -
-
- - {/* 日期範圍 - 結束 */} -
- -
- - setDateEnd(e.target.value)} - className="pl-9 block w-full text-left" - /> -
+ {/* 操作人員 */} +
+ +
-
+ {/* Row 2: Date Filters (Collapsible) */} + {showAdvancedFilter && ( +
+
+ +
+ {[ + { label: "今日", value: "today" }, + { label: "昨日", value: "yesterday" }, + { label: "本週", value: "this_week" }, + { label: "本月", value: "this_month" }, + { label: "上月", value: "last_month" }, + ].map((opt) => ( + + ))} +
+
+ +
+
+
+ +
+ + { + setDateStart(e.target.value); + setDateRangeType('custom'); + }} + // block w-full to ensure it fills space + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+ +
+ + { + setDateEnd(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+
+
+ )} + + {/* Action Bar */} +
+ -
@@ -264,32 +339,21 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u from={activities.from} /> -
-
- 每頁顯示 - - -
-
- -
+
+
setDetailOpen(false)} activity={selectedActivity} /> diff --git a/resources/js/Pages/PurchaseOrder/Index.tsx b/resources/js/Pages/PurchaseOrder/Index.tsx index b801b88..c5d9a88 100644 --- a/resources/js/Pages/PurchaseOrder/Index.tsx +++ b/resources/js/Pages/PurchaseOrder/Index.tsx @@ -3,7 +3,7 @@ */ import { useState, useEffect } from "react"; -import { Plus, ShoppingCart, Search, RotateCcw, Calendar } from 'lucide-react'; +import { Plus, ShoppingCart, Search, RotateCcw, Calendar, ChevronDown, ChevronUp } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router } from "@inertiajs/react"; @@ -11,6 +11,7 @@ import PurchaseOrderTable from "@/Components/PurchaseOrder/PurchaseOrderTable"; import type { PurchaseOrder } from "@/types/purchase-order"; import Pagination from "@/Components/shared/Pagination"; import { getBreadcrumbs } from "@/utils/breadcrumb"; +import { getDateRange } from "@/utils/format"; import { Can } from "@/Components/Permission/Can"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Input } from "@/Components/ui/input"; @@ -52,6 +53,12 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop const [dateStart, setDateStart] = useState(filters.date_start || ""); const [dateEnd, setDateEnd] = useState(filters.date_end || ""); const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [dateRangeType, setDateRangeType] = useState('custom'); + + // Advanced Filter Toggle + const [showAdvancedFilter, setShowAdvancedFilter] = useState( + !!(filters.date_start || filters.date_end) + ); // 同步 URL 參數到 State (雖有初始值,但若由外部連結進入可確保同步) useEffect(() => { @@ -86,10 +93,20 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop setWarehouseId("all"); setDateStart(""); setDateEnd(""); + setDateRangeType("custom"); router.get(route('purchase-orders.index')); }; + const handleDateRangeChange = (type: string) => { + setDateRangeType(type); + if (type === 'custom') return; + + const { start, end } = getDateRange(type); + setDateStart(start); + setDateEnd(end); + }; + const handlePerPageChange = (value: string) => { setPerPage(value); router.get( @@ -135,27 +152,26 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop {/* 篩選區塊 */}
-
- {/* 關鍵字搜尋 */} -
- + {/* Row 1: Search, Status, Warehouse */} +
+
+
- + setSearch(e.target.value)} - className="pl-9" + className="pl-10 h-9 block" onKeyDown={(e) => e.key === 'Enter' && handleFilter()} />
- {/* 狀態篩選 */} -
- +
+
- {/* 倉庫篩選 */} -
- +
+ ({ label: w.name, value: String(w.id) })) ]} placeholder="選擇倉庫" - className="w-full" + className="w-full h-9" />
- - {/* 日期範圍 - 開始 */} -
- -
- - setDateStart(e.target.value)} - className="pl-9 block w-full" - /> -
-
- - {/* 日期範圍 - 結束 */} -
- -
- - setDateEnd(e.target.value)} - className="pl-9 block w-full text-left" - /> -
-
-
+ {/* Row 2: Date Filters (Collapsible) */} + {showAdvancedFilter && ( +
+
+ +
+ {[ + { label: "今日", value: "today" }, + { label: "昨日", value: "yesterday" }, + { label: "本週", value: "this_week" }, + { label: "本月", value: "this_month" }, + { label: "上月", value: "last_month" }, + ].map((opt) => ( + + ))} +
+
+ +
+
+
+ +
+ + { + setDateStart(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+ +
+ + { + setDateEnd(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white text-left" + /> +
+
+
+
+
+ )} + +
+
diff --git a/resources/js/Pages/UtilityFee/Index.tsx b/resources/js/Pages/UtilityFee/Index.tsx index 8c6d4af..c43dea5 100644 --- a/resources/js/Pages/UtilityFee/Index.tsx +++ b/resources/js/Pages/UtilityFee/Index.tsx @@ -10,10 +10,12 @@ import { Trash2, FileText, Calendar, - Filter, + RotateCcw, ArrowUpDown, ArrowUp, - ArrowDown + ArrowDown, + ChevronDown, + ChevronUp } from 'lucide-react'; import { Label } from "@/Components/ui/label"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; @@ -41,7 +43,7 @@ import { AlertDialogTitle, } from "@/Components/ui/alert-dialog"; import { Can } from "@/Components/Permission/Can"; -import { formatDateWithDayOfWeek, formatInvoiceNumber } from "@/utils/format"; +import { formatDateWithDayOfWeek, formatInvoiceNumber, getDateRange } from "@/utils/format"; interface PageProps { fees: { @@ -71,15 +73,30 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: const [categoryFilter, setCategoryFilter] = useState(filters.category || "all"); const [dateStart, setDateStart] = useState(filters.date_start || ""); const [dateEnd, setDateEnd] = useState(filters.date_end || ""); + const [dateRangeType, setDateRangeType] = useState("custom"); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [editingFee, setEditingFee] = useState(null); const [deletingFeeId, setDeletingFeeId] = useState(null); + // Advanced Filter Toggle + const [showAdvancedFilter, setShowAdvancedFilter] = useState( + !!(filters.date_start || filters.date_end) + ); + // Sorting - const [sortField, setSortField] = useState(filters.sort_field || 'transaction_date'); - const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(filters.sort_direction as "asc" | "desc" || 'desc'); + const [sortField, setSortField] = useState(filters.sort_field || null); + const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(filters.sort_direction as "asc" | "desc" || null); + + const handleDateRangeChange = (type: string) => { + setDateRangeType(type); + if (type === "custom") return; + + const { start, end } = getDateRange(type); + setDateStart(start); + setDateEnd(end); + }; const handleSearch = () => { router.get( @@ -101,6 +118,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: setCategoryFilter("all"); setDateStart(""); setDateEnd(""); + setDateRangeType("custom"); router.get(route("utility-fees.index")); }; @@ -197,91 +215,150 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
- {/* Toolbar */} -
-
- {/* Search */} -
- -
- - setSearchTerm(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleSearch()} - className="pl-10" +
+
+ {/* Row 1: Search and Category */} +
+
+ +
+ + setSearchTerm(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + className="pl-10 h-9 block" + /> + {searchTerm && ( + + )} +
+
+
+ + ({ label: c, value: c })) + ]} + placeholder="篩選類別" + className="h-9" /> - {searchTerm && ( - +
+
+ + {/* Row 2: Date Filters (Collapsible) */} + {showAdvancedFilter && ( +
+
+ +
+ {[ + { label: "今日", value: "today" }, + { label: "昨日", value: "yesterday" }, + { label: "本週", value: "this_week" }, + { label: "本月", value: "this_month" }, + { label: "上月", value: "last_month" }, + ].map((opt) => ( + + ))} +
+
+ +
+
+
+ +
+ + { + setDateStart(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+ +
+ + { + setDateEnd(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+
+
+ )} + + {/* Action Buttons */} +
+
+ + +
- - {/* Date Range Start */} -
- -
- - setDateStart(e.target.value)} - className="pl-9 bg-white block w-full" - placeholder="開始日期" - /> -
-
- - {/* Date Range End */} -
- -
- - setDateEnd(e.target.value)} - className="pl-9 bg-white block w-full" - placeholder="結束日期" - /> -
-
- - {/* Category Filter */} -
- - ({ label: c, value: c })) - ]} - placeholder="篩選類別" - /> -
-
- -
- -
diff --git a/resources/js/utils/format.ts b/resources/js/utils/format.ts index 9e34f94..498f298 100644 --- a/resources/js/utils/format.ts +++ b/resources/js/utils/format.ts @@ -135,3 +135,51 @@ export const formatDateTime = (datetime: string): string => { hour12: false, }); }; + +/** + * 獲取日期區間(YYYY-MM-DD 格式) + * 支援: today, yesterday, this_week, this_month, last_month + */ +export const getDateRange = (type: string): { start: string, end: string } => { + const now = new Date(); + // Reset time to avoid timezone issues when calculating dates + now.setHours(12, 0, 0, 0); + + let start = new Date(now); + let end = new Date(now); + + const format = (d: Date) => { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + switch (type) { + case "today": + break; + case "yesterday": + start.setDate(now.getDate() - 1); + end.setDate(now.getDate() - 1); + break; + case "this_week": + // 週一為一週的第一天 + const dayOfWeek = now.getDay() || 7; + start.setDate(now.getDate() - dayOfWeek + 1); + end.setDate(now.getDate() + (7 - dayOfWeek)); + break; + case "this_month": + start = new Date(now.getFullYear(), now.getMonth(), 1); + end = new Date(now.getFullYear(), now.getMonth() + 1, 0); + break; + case "last_month": + start = new Date(now.getFullYear(), now.getMonth() - 1, 1); + end = new Date(now.getFullYear(), now.getMonth(), 0); + break; + } + + return { + start: format(start), + end: format(end) + }; +};