diff --git a/.agent/skills/ui-consistency/SKILL.md b/.agent/skills/ui-consistency/SKILL.md index af59d81..11b3959 100644 --- a/.agent/skills/ui-consistency/SKILL.md +++ b/.agent/skills/ui-consistency/SKILL.md @@ -57,13 +57,30 @@ tooltip ## 2. 色彩系統 -### 2.1 主題色 (Primary) +### 2.1 主題色 (Primary) - **動態租戶品牌色** -```css ---primary-main: #01ab83; /* 主題綠色 - 主要操作、連結 */ ---primary-dark: #018a6a; /* 深綠色 - Hover 狀態 */ ---primary-light: #33bc9a; /* 淺綠色 - 次要強調 */ ---primary-lightest: #e6f7f3; /* 最淺綠色 - 背景、Active 狀態 */ +> **注意**:主題色會根據租戶設定(Branding)動態改變,**嚴禁**在程式碼中 Hardcode 色碼(如 `#01ab83`)。 +> 請務必使用 Tailwind Utility Class 或 CSS 變數。 + +| Tailwind Class | CSS Variable | 說明 | +|----------------|--------------|------| +| `*-primary-main` | `--primary-main` | **主色**:與租戶設定一致(預設綠色),用於主要按鈕、連結、強調文字 | +| `*-primary-dark` | `--primary-dark` | **深色**:系統自動計算,用於 Hover 狀態 | +| `*-primary-light` | `--primary-light` | **淺色**:系統自動計算,用於次要強調 | +| `*-primary-lightest` | `--primary-lightest` | **最淺色**:系統自動計算,用於背景底色、Active 狀態 | + +**運作機制**: +`AuthenticatedLayout` 會根據後端回傳的 `branding` 資料,自動注入 CSS 變數覆寫預設值。 + +```tsx +// ✅ 正確:使用 Tailwind Class +
...
+ +// ✅ 正確:使用 CSS 變數 (自定義樣式時) +
...
+ +// ❌ 錯誤:寫死色碼 (會導致租戶無法換色) +
...
``` ### 2.2 灰階 (Grey Scale) @@ -341,6 +358,62 @@ import { Plus, Pencil, Trash2, Users } from 'lucide-react'; - 序號欄使用 `text-gray-500 font-medium text-center` - 操作欄使用 `flex items-center justify-center gap-2` 排列按鈕 +### 5.4 欄位排序規範 + +當表格需要支援排序時,請遵循以下模式: + +1. **圖標邏輯**: + * 未排序:`ArrowUpDown` (class: `text-muted-foreground`) + * 升冪 (asc):`ArrowUp` (class: `text-primary`) + * 降冪 (desc):`ArrowDown` (class: `text-primary`) +2. **結構**:在 `TableHead` 內使用 `button` 元素。 +3. **後端配合**:後端 Controller **必須** 處理 `sort_by` 與 `sort_order` 參數。 + +```tsx +// 1. 定義 Helper Component (在元件內部) +const SortIcon = ({ field }: { field: string }) => { + if (filters.sort_by !== field) { + return ; + } + if (filters.sort_order === "asc") { + return ; + } + return ; +}; + +// 2. 表格標題應用 + + + + +// 3. 排序處理函式 (三態切換:未排序 -> 升冪 -> 降冪 -> 未排序) +const handleSort = (field: string) => { + let newSortBy: string | undefined = field; + let newSortOrder: 'asc' | 'desc' | undefined = 'asc'; + + if (filters.sort_by === field) { + if (filters.sort_order === 'asc') { + newSortOrder = 'desc'; + } else { + // desc -> reset (回到預設排序) + newSortBy = undefined; + newSortOrder = undefined; + } + } + + router.get( + route(route().current()!), + { ...filters, sort_by: newSortBy, sort_order: newSortOrder }, + { preserveState: true, replace: true } + ); +}; +``` + --- ## 6. 分頁規範 diff --git a/app/Http/Controllers/Admin/ActivityLogController.php b/app/Http/Controllers/Admin/ActivityLogController.php index 5897dba..340fe27 100644 --- a/app/Http/Controllers/Admin/ActivityLogController.php +++ b/app/Http/Controllers/Admin/ActivityLogController.php @@ -11,9 +11,19 @@ class ActivityLogController extends Controller { public function index(Request $request) { - $activities = Activity::with('causer') - ->latest() - ->paginate($request->input('per_page', 10)) + $perPage = $request->input('per_page', 10); + $sortBy = $request->input('sort_by', 'created_at'); + $sortOrder = $request->input('sort_order', 'desc'); + + $query = Activity::with('causer'); + + if ($sortBy === 'created_at') { + $query->orderBy($sortBy, $sortOrder); + } else { + $query->latest(); + } + + $activities = $query->paginate($perPage) ->through(function ($activity) { $subjectMap = [ 'App\Models\User' => '使用者', @@ -46,6 +56,8 @@ class ActivityLogController extends Controller 'activities' => $activities, 'filters' => [ 'per_page' => $request->input('per_page', '10'), + 'sort_by' => $request->input('sort_by'), + 'sort_order' => $request->input('sort_order'), ], ]); } diff --git a/app/Http/Controllers/Admin/RoleController.php b/app/Http/Controllers/Admin/RoleController.php index fd321e0..b51afa3 100644 --- a/app/Http/Controllers/Admin/RoleController.php +++ b/app/Http/Controllers/Admin/RoleController.php @@ -14,15 +14,26 @@ class RoleController extends Controller /** * Display a listing of the resource. */ - public function index() + public function index(Request $request) { - $roles = Role::withCount('users', 'permissions') - ->with('users:id,name,username') - ->orderBy('id') - ->get(); + $sortBy = $request->input('sort_by', 'id'); + $sortOrder = $request->input('sort_order', 'asc'); + + $query = Role::withCount('users', 'permissions') + ->with('users:id,name,username'); + + // Handle sorting + if (in_array($sortBy, ['users_count', 'permissions_count', 'created_at', 'id'])) { + $query->orderBy($sortBy, $sortOrder); + } else { + $query->orderBy('id', 'asc'); + } + + $roles = $query->get(); return Inertia::render('Admin/Role/Index', [ - 'roles' => $roles + 'roles' => $roles, + 'filters' => $request->only(['sort_by', 'sort_order']), ]); } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index cbd9da6..85dba9c 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -18,15 +18,23 @@ class UserController extends Controller public function index(Request $request) { $perPage = $request->input('per_page', 10); + $sortBy = $request->input('sort_by', 'id'); + $sortOrder = $request->input('sort_order', 'asc'); - $users = User::with(['roles:id,name,display_name']) - ->orderBy('id') - ->paginate($perPage) - ->withQueryString(); + $query = User::with(['roles:id,name,display_name']); + + // Handle sorting + if (in_array($sortBy, ['name', 'created_at'])) { + $query->orderBy($sortBy, $sortOrder); + } else { + $query->orderBy('id', 'asc'); + } + + $users = $query->paginate($perPage)->withQueryString(); return Inertia::render('Admin/User/Index', [ 'users' => $users, - 'filters' => $request->only(['per_page']), + 'filters' => $request->only(['per_page', 'sort_by', 'sort_order']), ]); } diff --git a/resources/js/Components/Product/ProductTable.tsx b/resources/js/Components/Product/ProductTable.tsx index 35f68c6..6f77620 100644 --- a/resources/js/Components/Product/ProductTable.tsx +++ b/resources/js/Components/Product/ProductTable.tsx @@ -75,22 +75,22 @@ export default function ProductTable({ # - - - - diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderTable.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderTable.tsx index b8df57f..eafe7a1 100644 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderTable.tsx +++ b/resources/js/Components/PurchaseOrder/PurchaseOrderTable.tsx @@ -123,7 +123,7 @@ export default function PurchaseOrderTable({ - 操作 + 操作 diff --git a/resources/js/Components/Vendor/SupplyProductList.tsx b/resources/js/Components/Vendor/SupplyProductList.tsx index de3b3b7..1b52482 100644 --- a/resources/js/Components/Vendor/SupplyProductList.tsx +++ b/resources/js/Components/Vendor/SupplyProductList.tsx @@ -27,14 +27,14 @@ export default function SupplyProductList({ # - 商品名稱 - 基本單位 - 轉換率 - + 商品名稱 + 基本單位 + 轉換率 + 上次採購單價
(以基本單位計算)
- 操作 + 操作
diff --git a/resources/js/Components/Vendor/VendorTable.tsx b/resources/js/Components/Vendor/VendorTable.tsx index 4adcf20..693db7d 100644 --- a/resources/js/Components/Vendor/VendorTable.tsx +++ b/resources/js/Components/Vendor/VendorTable.tsx @@ -61,27 +61,27 @@ export default function VendorTable({ # - - - - - diff --git a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx index baacdde..9914719 100644 --- a/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx +++ b/resources/js/Components/Warehouse/Inventory/InventoryTable.tsx @@ -183,37 +183,37 @@ export default function InventoryTable({ # -
-
- -
-
-
diff --git a/resources/js/Pages/Admin/ActivityLog/Index.tsx b/resources/js/Pages/Admin/ActivityLog/Index.tsx index 3c9b052..9769522 100644 --- a/resources/js/Pages/Admin/ActivityLog/Index.tsx +++ b/resources/js/Pages/Admin/ActivityLog/Index.tsx @@ -13,7 +13,7 @@ import { import { Badge } from "@/Components/ui/badge"; import Pagination from '@/Components/shared/Pagination'; import { SearchableSelect } from "@/Components/ui/searchable-select"; -import { FileText, Eye } from 'lucide-react'; +import { FileText, Eye, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import { format } from 'date-fns'; import { Button } from '@/Components/ui/button'; import ActivityDetailDialog from './ActivityDetailDialog'; @@ -45,6 +45,8 @@ interface Props extends PageProps { }; filters: { per_page?: string; + sort_by?: string; + sort_order?: 'asc' | 'desc'; }; } @@ -82,11 +84,41 @@ export default function ActivityLogIndex({ activities, filters }: Props) { setPerPage(value); router.get( route('activity-logs.index'), - { per_page: value }, + { ...filters, per_page: value }, { preserveState: false, replace: true, preserveScroll: true } ); }; + const handleSort = (field: string) => { + let newSortBy: string | undefined = field; + let newSortOrder: 'asc' | 'desc' | undefined = 'asc'; + + if (filters.sort_by === field) { + if (filters.sort_order === 'asc') { + newSortOrder = 'desc'; + } else { + newSortBy = undefined; + newSortOrder = undefined; + } + } + + router.get( + route('activity-logs.index'), + { ...filters, sort_by: newSortBy, sort_order: newSortOrder }, + { preserveState: true, replace: true } + ); + }; + + const SortIcon = ({ field }: { field: string }) => { + if (filters.sort_by !== field) { + return ; + } + if (filters.sort_order === "asc") { + return ; + } + return ; + }; + return ( - 時間 + # + + + 操作人員 動作 對象 @@ -123,8 +163,11 @@ export default function ActivityLogIndex({ activities, filters }: Props) { {activities.data.length > 0 ? ( - activities.data.map((activity) => ( + activities.data.map((activity, index) => ( + + {activities.from + index} + {activity.created_at} @@ -154,7 +197,8 @@ export default function ActivityLogIndex({ activities, filters }: Props) { variant="outline" size="sm" onClick={() => handleViewDetail(activity)} - className="button-outlined-info" + className="button-outlined-primary" + title="檢視詳情" > @@ -163,7 +207,7 @@ export default function ActivityLogIndex({ activities, filters }: Props) { )) ) : ( - + 尚無操作紀錄 diff --git a/resources/js/Pages/Admin/Role/Index.tsx b/resources/js/Pages/Admin/Role/Index.tsx index 234503d..df0e084 100644 --- a/resources/js/Pages/Admin/Role/Index.tsx +++ b/resources/js/Pages/Admin/Role/Index.tsx @@ -1,7 +1,7 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, router } from '@inertiajs/react'; import { cn } from "@/lib/utils"; -import { Shield, Plus, Pencil, Trash2, Users } from 'lucide-react'; +import { Shield, Plus, Pencil, Trash2, Users, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import { Button } from '@/Components/ui/button'; import { Badge } from '@/Components/ui/badge'; import { @@ -42,9 +42,13 @@ interface Role { interface Props { roles: Role[]; + filters: { + sort_by?: string; + sort_order?: 'asc' | 'desc'; + }; } -export default function RoleIndex({ roles }: Props) { +export default function RoleIndex({ roles, filters = {} }: Props) { const [selectedRole, setSelectedRole] = useState(null); const handleDelete = (id: number, name: string) => { @@ -55,6 +59,36 @@ export default function RoleIndex({ roles }: Props) { } }; + const handleSort = (field: string) => { + let newSortBy: string | undefined = field; + let newSortOrder: 'asc' | 'desc' | undefined = 'asc'; + + if (filters.sort_by === field) { + if (filters.sort_order === 'asc') { + newSortOrder = 'desc'; + } else { + newSortBy = undefined; + newSortOrder = undefined; + } + } + + router.get( + route('roles.index'), + { ...filters, sort_by: newSortBy, sort_order: newSortOrder }, + { preserveState: true, replace: true } + ); + }; + + const SortIcon = ({ field }: { field: string }) => { + if (filters.sort_by !== field) { + return ; + } + if (filters.sort_order === "asc") { + return ; + } + return ; + }; + return ( #
名稱 代號 - 權限數量 - 使用者人數 - 建立時間 + + + + + + + + + 操作
diff --git a/resources/js/Pages/Admin/User/Index.tsx b/resources/js/Pages/Admin/User/Index.tsx index 6ec1be1..52a3ccb 100644 --- a/resources/js/Pages/Admin/User/Index.tsx +++ b/resources/js/Pages/Admin/User/Index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { Head, Link, router } from '@inertiajs/react'; -import { Users, Plus, Pencil, Trash2, Mail, Shield } from 'lucide-react'; +import { Users, Plus, Pencil, Trash2, Mail, Shield, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import { Button } from '@/Components/ui/button'; import { Table, @@ -47,6 +47,8 @@ interface Props { }; filters: { per_page?: string; + sort_by?: string; + sort_order?: 'asc' | 'desc'; }; } @@ -66,11 +68,41 @@ export default function UserIndex({ users, filters }: Props) { setPerPage(value); router.get( route('users.index'), - { per_page: value }, + { ...filters, per_page: value }, { preserveState: false, replace: true, preserveScroll: true } ); }; + const handleSort = (field: string) => { + let newSortBy: string | undefined = field; + let newSortOrder: 'asc' | 'desc' | undefined = 'asc'; + + if (filters.sort_by === field) { + if (filters.sort_order === 'asc') { + newSortOrder = 'desc'; + } else { + newSortBy = undefined; + newSortOrder = undefined; + } + } + + router.get( + route('users.index'), + { ...filters, sort_by: newSortBy, sort_order: newSortOrder }, + { preserveState: true, replace: true } + ); + }; + + const SortIcon = ({ field }: { field: string }) => { + if (filters.sort_by !== field) { + return ; + } + if (filters.sort_order === "asc") { + return ; + } + return ; + }; + return ( @@ -108,9 +140,23 @@ export default function UserIndex({ users, filters }: Props) { # - 使用者 + + + 角色 - 加入時間 + + + 操作