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({
#
-
- onSort("name")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("name")} className="flex items-center hover:text-gray-900">
商品名稱
- onSort("category_id")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("category_id")} className="flex items-center hover:text-gray-900">
分類
- onSort("base_unit_id")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("base_unit_id")} className="flex items-center hover:text-gray-900">
基本單位
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({
handleSort("poNumber")}
- className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 hover:text-foreground transition-colors"
>
採購單編號
@@ -132,7 +132,7 @@ export default function PurchaseOrderTable({
handleSort("warehouse_name")}
- className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 hover:text-foreground transition-colors"
>
預計入庫倉庫
@@ -141,7 +141,7 @@ export default function PurchaseOrderTable({
handleSort("supplierName")}
- className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 hover:text-foreground transition-colors"
>
供應商
@@ -150,7 +150,7 @@ export default function PurchaseOrderTable({
handleSort("createdAt")}
- className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 hover:text-foreground transition-colors"
>
建立日期
@@ -159,7 +159,7 @@ export default function PurchaseOrderTable({
handleSort("totalAmount")}
- className="flex items-center gap-2 ml-auto hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 ml-auto hover:text-foreground transition-colors"
>
總金額
@@ -168,13 +168,13 @@ export default function PurchaseOrderTable({
handleSort("status")}
- className="flex items-center gap-2 hover:text-foreground transition-colors font-semibold"
+ className="flex items-center gap-2 hover:text-foreground transition-colors"
>
狀態
- 操作
+ 操作
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({
#
- onSort("code")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("code")} className="flex items-center hover:text-gray-900">
編號
- onSort("name")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("name")} className="flex items-center hover:text-gray-900">
廠商名稱
- onSort("owner")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("owner")} className="flex items-center hover:text-gray-900">
負責人
- onSort("contact_name")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("contact_name")} className="flex items-center hover:text-gray-900">
聯絡人
- onSort("phone")} className="flex items-center hover:text-gray-900 font-semibold">
+ onSort("phone")} className="flex items-center hover:text-gray-900">
聯絡電話
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({
#
- handleSort("productName")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("productName")} className="flex items-center hover:text-gray-900">
商品資訊
- handleSort("quantity")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("quantity")} className="flex items-center hover:text-gray-900">
庫存數量
- handleSort("lastInboundDate")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("lastInboundDate")} className="flex items-center hover:text-gray-900">
最新入庫
- handleSort("lastOutboundDate")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("lastOutboundDate")} className="flex items-center hover:text-gray-900">
最新出庫
- handleSort("safetyStock")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("safetyStock")} className="flex items-center hover:text-gray-900">
安全庫存
- handleSort("status")} className="flex items-center hover:text-gray-900 font-semibold">
+ handleSort("status")} className="flex items-center hover:text-gray-900">
狀態
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 (
- 時間
+ #
+
+ handleSort('created_at')}
+ className="flex items-center gap-1 hover:text-gray-900 transition-colors"
+ >
+ 時間
+
+
操作人員
動作
對象
@@ -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 (
#
名稱
代號
- 權限數量
- 使用者人數
- 建立時間
+
+ handleSort('permissions_count')}
+ className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
+ >
+ 權限數量
+
+
+
+ handleSort('users_count')}
+ className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
+ >
+ 使用者人數
+
+
+
+ handleSort('created_at')}
+ className="flex items-center gap-1 hover:text-gray-900 transition-colors"
+ >
+ 建立時間
+
+
操作
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) {
#
- 使用者
+
+ handleSort('name')}
+ className="flex items-center gap-1 hover:text-gray-900 transition-colors"
+ >
+ 使用者
+
+
角色
- 加入時間
+
+ handleSort('created_at')}
+ className="flex items-center gap-1 hover:text-gray-900 transition-colors"
+ >
+ 加入時間
+
+
操作