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 && (