diff --git a/.agent/skills/ui-consistency/SKILL.md b/.agent/skills/ui-consistency/SKILL.md index 47440d8..be2803d 100644 --- a/.agent/skills/ui-consistency/SKILL.md +++ b/.agent/skills/ui-consistency/SKILL.md @@ -732,6 +732,51 @@ import { SearchableSelect } from "@/Components/ui/searchable-select"; --- +## 11.5 輸入框尺寸 (Input Sizes) + +為確保介面整齊與統一,所有表單輸入元件標準高度應為 **`h-9`** (36px),與標準按鈕尺寸對齊。 + +- **Input**: 預設即為 `h-9` (由 `py-1` 與 `text-sm` 組合而成) +- **Select / SearchableSelect**: 必須確保 Trigger 按鈕高度為 `h-9` +- **禁止使用**: 除非有特殊設計需求,否則避免使用 `h-10` (40px) 或其他非標準高度。 + +## 11.6 日期輸入框樣式 (Date Input Style) + +日期輸入框應採用「**左側裝飾圖示 + 右側原生操作**」的配置,以保持視覺一致性並保留瀏覽器原生便利性。 + +**樣式規格**: +1. **容器**: 使用 `relative` 定位。 +2. **圖標**: 使用 `Calendar` 圖標,放置於絕對位置 `absolute left-2.5 top-2.5`,顏色 `text-gray-400`,並設定 `pointer-events-none` 避免干擾點擊。 +3. **輸入框**: 設定 `pl-9` (左內距) 以避開圖示,並使用原生 `type="date"` 或 `type="datetime-local"`。 + +```tsx +import { Calendar } from "lucide-react"; +import { Input } from "@/Components/ui/input"; + +
+ + setDate(e.target.value)} + /> +
+``` + +## 11.7 搜尋選單樣式 (SearchableSelect Style) + +`SearchableSelect` 元件在表單或篩選列中使用時,高度必須設定為 `h-9` 以與輸入框對齊。 + +```tsx + +``` + +--- + ## 12. 檢查清單 在開發或審查頁面時,請確認以下項目: diff --git a/app/Http/Controllers/UtilityFeeController.php b/app/Http/Controllers/UtilityFeeController.php index c92572f..e67ec25 100644 --- a/app/Http/Controllers/UtilityFeeController.php +++ b/app/Http/Controllers/UtilityFeeController.php @@ -36,9 +36,14 @@ class UtilityFeeController extends Controller } // Sorting - $sortField = $request->input('sort_field', 'transaction_date'); - $sortDirection = $request->input('sort_direction', 'desc'); - $query->orderBy($sortField, $sortDirection); + $sortField = $request->input('sort_field'); + $sortDirection = $request->input('sort_direction'); + + if ($sortField && $sortDirection) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('transaction_date', 'desc'); + } $fees = $query->paginate($request->input('per_page', 15))->withQueryString(); diff --git a/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx b/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx index 23ed8ec..90de36f 100644 --- a/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx +++ b/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx @@ -14,6 +14,7 @@ import { Textarea } from "@/Components/ui/textarea"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { useForm } from "@inertiajs/react"; import { toast } from "sonner"; +import { Calendar } from "lucide-react"; export interface UtilityFee { id: number; @@ -65,7 +66,7 @@ export default function UtilityFeeDialog({ clearErrors(); if (fee) { setData({ - transaction_date: fee.transaction_date, + transaction_date: fee.transaction_date.split("T")[0].split(" ")[0], category: fee.category, amount: fee.amount.toString(), invoice_number: fee.invoice_number || "", @@ -122,14 +123,17 @@ export default function UtilityFeeDialog({ - setData("transaction_date", e.target.value)} - className={errors.transaction_date ? "border-red-500" : ""} - required - /> +
+ + setData("transaction_date", e.target.value)} + className={`pl-9 ${errors.transaction_date ? "border-red-500" : ""}`} + required + /> +
{errors.transaction_date &&

{errors.transaction_date}

} @@ -172,9 +176,26 @@ export default function UtilityFeeDialog({ setData("invoice_number", e.target.value)} - placeholder="例:AB12345678" + onChange={(e) => { + let value = e.target.value.toUpperCase(); + // Remove non-alphanumeric chars + const raw = value.replace(/[^A-Z0-9]/g, ''); + + // Auto-insert hyphen after 2 chars if we have length > 2 + if (raw.length > 2) { + value = `${raw.slice(0, 2)}-${raw.slice(2)}`; + } else { + value = raw; + } + + // Limit max length (2 letters + 8 digits + 1 hyphen = 11 chars) + if (value.length > 11) value = value.slice(0, 11); + + setData("invoice_number", value); + }} + placeholder="例:AB-12345678" /> +

格式:AB-12345678 (系統自動格式化)

{errors.invoice_number &&

{errors.invoice_number}

} diff --git a/resources/js/Pages/UtilityFee/Index.tsx b/resources/js/Pages/UtilityFee/Index.tsx index 1091feb..5c874e8 100644 --- a/resources/js/Pages/UtilityFee/Index.tsx +++ b/resources/js/Pages/UtilityFee/Index.tsx @@ -10,7 +10,10 @@ import { Trash2, FileText, Calendar, - Filter + Filter, + ArrowUpDown, + ArrowUp, + ArrowDown } from 'lucide-react'; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router } from "@inertiajs/react"; @@ -23,6 +26,7 @@ import { TableHeader, TableRow, } from "@/Components/ui/table"; +import { Badge } from "@/Components/ui/badge"; import { toast } from "sonner"; import UtilityFeeDialog, { UtilityFee } from "@/Components/UtilityFee/UtilityFeeDialog"; import { @@ -35,6 +39,8 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/Components/ui/alert-dialog"; +import { Can } from "@/Components/Permission/Can"; +import { formatDateWithDayOfWeek, formatInvoiceNumber } from "@/utils/format"; interface PageProps { fees: { @@ -53,8 +59,8 @@ interface PageProps { category?: string; date_start?: string; date_end?: string; - sort_field?: string; - sort_direction?: string; + sort_field?: string | null; + sort_direction?: "asc" | "desc" | null; per_page?: string; }; } @@ -71,8 +77,8 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: const [deletingFeeId, setDeletingFeeId] = useState(null); // Sorting - const [sortField, setSortField] = useState(filters.sort_field || 'transaction_date'); - const [sortDirection, setSortDirection] = useState(filters.sort_direction || 'desc'); + const [sortField, setSortField] = useState(filters.sort_field || 'transaction_date'); + const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(filters.sort_direction as "asc" | "desc" || 'desc'); const handleSearch = () => { router.get( @@ -98,9 +104,21 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: }; const handleSort = (field: string) => { - const newDirection = sortField === field && sortDirection === 'asc' ? 'desc' : 'asc'; - setSortField(field); + let newField: string | null = field; + let newDirection: "asc" | "desc" | null = "asc"; + + if (sortField === field) { + if (sortDirection === "asc") { + newDirection = "desc"; + } else { + newDirection = null; + newField = null; + } + } + + setSortField(newField); setSortDirection(newDirection); + router.get( route("utility-fees.index"), { @@ -108,7 +126,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: category: categoryFilter, date_start: dateStart, date_end: dateEnd, - sort_field: field, + sort_field: newField, sort_direction: newDirection, }, { preserveState: true } @@ -141,6 +159,19 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: } }; + const SortIcon = ({ field }: { field: string }) => { + if (sortField !== field) { + return ; + } + if (sortDirection === "asc") { + return ; + } + if (sortDirection === "desc") { + return ; + } + return ; + }; + return ( @@ -155,12 +186,14 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:

管理店鋪水、電、瓦斯等各項公共事業費用支出

- + + + {/* Toolbar */} @@ -174,7 +207,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} - className="pl-10 h-10" + className="pl-10" /> {searchTerm && (