style: 統一所有表格標題樣式為一般粗細並修正排序功能
This commit is contained in:
@@ -57,13 +57,30 @@ tooltip
|
|||||||
|
|
||||||
## 2. 色彩系統
|
## 2. 色彩系統
|
||||||
|
|
||||||
### 2.1 主題色 (Primary)
|
### 2.1 主題色 (Primary) - **動態租戶品牌色**
|
||||||
|
|
||||||
```css
|
> **注意**:主題色會根據租戶設定(Branding)動態改變,**嚴禁**在程式碼中 Hardcode 色碼(如 `#01ab83`)。
|
||||||
--primary-main: #01ab83; /* 主題綠色 - 主要操作、連結 */
|
> 請務必使用 Tailwind Utility Class 或 CSS 變數。
|
||||||
--primary-dark: #018a6a; /* 深綠色 - Hover 狀態 */
|
|
||||||
--primary-light: #33bc9a; /* 淺綠色 - 次要強調 */
|
| Tailwind Class | CSS Variable | 說明 |
|
||||||
--primary-lightest: #e6f7f3; /* 最淺綠色 - 背景、Active 狀態 */
|
|----------------|--------------|------|
|
||||||
|
| `*-primary-main` | `--primary-main` | **主色**:與租戶設定一致(預設綠色),用於主要按鈕、連結、強調文字 |
|
||||||
|
| `*-primary-dark` | `--primary-dark` | **深色**:系統自動計算,用於 Hover 狀態 |
|
||||||
|
| `*-primary-light` | `--primary-light` | **淺色**:系統自動計算,用於次要強調 |
|
||||||
|
| `*-primary-lightest` | `--primary-lightest` | **最淺色**:系統自動計算,用於背景底色、Active 狀態 |
|
||||||
|
|
||||||
|
**運作機制**:
|
||||||
|
`AuthenticatedLayout` 會根據後端回傳的 `branding` 資料,自動注入 CSS 變數覆寫預設值。
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 正確:使用 Tailwind Class
|
||||||
|
<div className="text-primary-main">...</div>
|
||||||
|
|
||||||
|
// ✅ 正確:使用 CSS 變數 (自定義樣式時)
|
||||||
|
<div style={{ borderColor: 'var(--primary-main)' }}>...</div>
|
||||||
|
|
||||||
|
// ❌ 錯誤:寫死色碼 (會導致租戶無法換色)
|
||||||
|
<div className="text-[#01ab83]">...</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 灰階 (Grey Scale)
|
### 2.2 灰階 (Grey Scale)
|
||||||
@@ -341,6 +358,62 @@ import { Plus, Pencil, Trash2, Users } from 'lucide-react';
|
|||||||
- 序號欄使用 `text-gray-500 font-medium text-center`
|
- 序號欄使用 `text-gray-500 font-medium text-center`
|
||||||
- 操作欄使用 `flex items-center justify-center gap-2` 排列按鈕
|
- 操作欄使用 `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 <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
|
||||||
|
}
|
||||||
|
if (filters.sort_order === "asc") {
|
||||||
|
return <ArrowUp className="h-4 w-4 text-primary ml-1" />;
|
||||||
|
}
|
||||||
|
return <ArrowDown className="h-4 w-4 text-primary ml-1" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 表格標題應用
|
||||||
|
<TableHead>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('created_at')}
|
||||||
|
className="flex items-center hover:text-gray-900"
|
||||||
|
>
|
||||||
|
建立時間 <SortIcon field="created_at" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
// 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. 分頁規範
|
## 6. 分頁規範
|
||||||
|
|||||||
@@ -11,9 +11,19 @@ class ActivityLogController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$activities = Activity::with('causer')
|
$perPage = $request->input('per_page', 10);
|
||||||
->latest()
|
$sortBy = $request->input('sort_by', 'created_at');
|
||||||
->paginate($request->input('per_page', 10))
|
$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) {
|
->through(function ($activity) {
|
||||||
$subjectMap = [
|
$subjectMap = [
|
||||||
'App\Models\User' => '使用者',
|
'App\Models\User' => '使用者',
|
||||||
@@ -46,6 +56,8 @@ class ActivityLogController extends Controller
|
|||||||
'activities' => $activities,
|
'activities' => $activities,
|
||||||
'filters' => [
|
'filters' => [
|
||||||
'per_page' => $request->input('per_page', '10'),
|
'per_page' => $request->input('per_page', '10'),
|
||||||
|
'sort_by' => $request->input('sort_by'),
|
||||||
|
'sort_order' => $request->input('sort_order'),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,26 @@ class RoleController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$roles = Role::withCount('users', 'permissions')
|
$sortBy = $request->input('sort_by', 'id');
|
||||||
->with('users:id,name,username')
|
$sortOrder = $request->input('sort_order', 'asc');
|
||||||
->orderBy('id')
|
|
||||||
->get();
|
$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', [
|
return Inertia::render('Admin/Role/Index', [
|
||||||
'roles' => $roles
|
'roles' => $roles,
|
||||||
|
'filters' => $request->only(['sort_by', 'sort_order']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,23 @@ class UserController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$perPage = $request->input('per_page', 10);
|
$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'])
|
$query = User::with(['roles:id,name,display_name']);
|
||||||
->orderBy('id')
|
|
||||||
->paginate($perPage)
|
// Handle sorting
|
||||||
->withQueryString();
|
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', [
|
return Inertia::render('Admin/User/Index', [
|
||||||
'users' => $users,
|
'users' => $users,
|
||||||
'filters' => $request->only(['per_page']),
|
'filters' => $request->only(['per_page', 'sort_by', 'sort_order']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,22 +75,22 @@ export default function ProductTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("code")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("code")} className="flex items-center hover:text-gray-900">
|
||||||
商品編號 <SortIcon field="code" />
|
商品編號 <SortIcon field="code" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("name")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("name")} className="flex items-center hover:text-gray-900">
|
||||||
商品名稱 <SortIcon field="name" />
|
商品名稱 <SortIcon field="name" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("category_id")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("category_id")} className="flex items-center hover:text-gray-900">
|
||||||
分類 <SortIcon field="category_id" />
|
分類 <SortIcon field="category_id" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("base_unit_id")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("base_unit_id")} className="flex items-center hover:text-gray-900">
|
||||||
基本單位 <SortIcon field="base_unit_id" />
|
基本單位 <SortIcon field="base_unit_id" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[180px]">
|
<TableHead className="w-[180px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("poNumber")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
採購單編號
|
採購單編號
|
||||||
<SortIcon field="poNumber" />
|
<SortIcon field="poNumber" />
|
||||||
@@ -132,7 +132,7 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[200px]">
|
<TableHead className="w-[200px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("warehouse_name")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
預計入庫倉庫
|
預計入庫倉庫
|
||||||
<SortIcon field="warehouse_name" />
|
<SortIcon field="warehouse_name" />
|
||||||
@@ -141,7 +141,7 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[180px]">
|
<TableHead className="w-[180px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("supplierName")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
供應商
|
供應商
|
||||||
<SortIcon field="supplierName" />
|
<SortIcon field="supplierName" />
|
||||||
@@ -150,7 +150,7 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[150px]">
|
<TableHead className="w-[150px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("createdAt")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
建立日期
|
建立日期
|
||||||
<SortIcon field="createdAt" />
|
<SortIcon field="createdAt" />
|
||||||
@@ -159,7 +159,7 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[140px] text-right">
|
<TableHead className="w-[140px] text-right">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("totalAmount")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
總金額
|
總金額
|
||||||
<SortIcon field="totalAmount" />
|
<SortIcon field="totalAmount" />
|
||||||
@@ -168,13 +168,13 @@ export default function PurchaseOrderTable({
|
|||||||
<TableHead className="w-[120px]">
|
<TableHead className="w-[120px]">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSort("status")}
|
onClick={() => 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"
|
||||||
>
|
>
|
||||||
狀態
|
狀態
|
||||||
<SortIcon field="status" />
|
<SortIcon field="status" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-center font-semibold">操作</TableHead>
|
<TableHead className="text-center">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ export default function SupplyProductList({
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead className="font-semibold">商品名稱</TableHead>
|
<TableHead>商品名稱</TableHead>
|
||||||
<TableHead className="font-semibold">基本單位</TableHead>
|
<TableHead>基本單位</TableHead>
|
||||||
<TableHead className="font-semibold">轉換率</TableHead>
|
<TableHead>轉換率</TableHead>
|
||||||
<TableHead className="text-right font-semibold">
|
<TableHead className="text-right">
|
||||||
上次採購單價
|
上次採購單價
|
||||||
<div className="text-xs font-normal text-muted-foreground">(以基本單位計算)</div>
|
<div className="text-xs font-normal text-muted-foreground">(以基本單位計算)</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-center font-semibold w-[150px]">操作</TableHead>
|
<TableHead className="text-center w-[150px]">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|||||||
10
resources/js/Components/Vendor/VendorTable.tsx
vendored
10
resources/js/Components/Vendor/VendorTable.tsx
vendored
@@ -61,27 +61,27 @@ export default function VendorTable({
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("code")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("code")} className="flex items-center hover:text-gray-900">
|
||||||
編號 <SortIcon field="code" />
|
編號 <SortIcon field="code" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("name")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("name")} className="flex items-center hover:text-gray-900">
|
||||||
廠商名稱 <SortIcon field="name" />
|
廠商名稱 <SortIcon field="name" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("owner")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("owner")} className="flex items-center hover:text-gray-900">
|
||||||
負責人 <SortIcon field="owner" />
|
負責人 <SortIcon field="owner" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("contact_name")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("contact_name")} className="flex items-center hover:text-gray-900">
|
||||||
聯絡人 <SortIcon field="contact_name" />
|
聯絡人 <SortIcon field="contact_name" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<button onClick={() => onSort("phone")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => onSort("phone")} className="flex items-center hover:text-gray-900">
|
||||||
聯絡電話 <SortIcon field="phone" />
|
聯絡電話 <SortIcon field="phone" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@@ -183,37 +183,37 @@ export default function InventoryTable({
|
|||||||
<TableRow className="bg-gray-50/50">
|
<TableRow className="bg-gray-50/50">
|
||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead className="w-[25%]">
|
<TableHead className="w-[25%]">
|
||||||
<button onClick={() => handleSort("productName")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("productName")} className="flex items-center hover:text-gray-900">
|
||||||
商品資訊 <SortIcon field="productName" />
|
商品資訊 <SortIcon field="productName" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[10%] text-right">
|
<TableHead className="w-[10%] text-right">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button onClick={() => handleSort("quantity")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("quantity")} className="flex items-center hover:text-gray-900">
|
||||||
庫存數量 <SortIcon field="quantity" />
|
庫存數量 <SortIcon field="quantity" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[12%]">
|
<TableHead className="w-[12%]">
|
||||||
<button onClick={() => handleSort("lastInboundDate")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("lastInboundDate")} className="flex items-center hover:text-gray-900">
|
||||||
最新入庫 <SortIcon field="lastInboundDate" />
|
最新入庫 <SortIcon field="lastInboundDate" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[12%]">
|
<TableHead className="w-[12%]">
|
||||||
<button onClick={() => handleSort("lastOutboundDate")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("lastOutboundDate")} className="flex items-center hover:text-gray-900">
|
||||||
最新出庫 <SortIcon field="lastOutboundDate" />
|
最新出庫 <SortIcon field="lastOutboundDate" />
|
||||||
</button>
|
</button>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[10%] text-right">
|
<TableHead className="w-[10%] text-right">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button onClick={() => handleSort("safetyStock")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("safetyStock")} className="flex items-center hover:text-gray-900">
|
||||||
安全庫存 <SortIcon field="safetyStock" />
|
安全庫存 <SortIcon field="safetyStock" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[10%] text-center">
|
<TableHead className="w-[10%] text-center">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<button onClick={() => handleSort("status")} className="flex items-center hover:text-gray-900 font-semibold">
|
<button onClick={() => handleSort("status")} className="flex items-center hover:text-gray-900">
|
||||||
狀態 <SortIcon field="status" />
|
狀態 <SortIcon field="status" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { Badge } from "@/Components/ui/badge";
|
import { Badge } from "@/Components/ui/badge";
|
||||||
import Pagination from '@/Components/shared/Pagination';
|
import Pagination from '@/Components/shared/Pagination';
|
||||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
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 { format } from 'date-fns';
|
||||||
import { Button } from '@/Components/ui/button';
|
import { Button } from '@/Components/ui/button';
|
||||||
import ActivityDetailDialog from './ActivityDetailDialog';
|
import ActivityDetailDialog from './ActivityDetailDialog';
|
||||||
@@ -45,6 +45,8 @@ interface Props extends PageProps {
|
|||||||
};
|
};
|
||||||
filters: {
|
filters: {
|
||||||
per_page?: string;
|
per_page?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +84,41 @@ export default function ActivityLogIndex({ activities, filters }: Props) {
|
|||||||
setPerPage(value);
|
setPerPage(value);
|
||||||
router.get(
|
router.get(
|
||||||
route('activity-logs.index'),
|
route('activity-logs.index'),
|
||||||
{ per_page: value },
|
{ ...filters, per_page: value },
|
||||||
{ preserveState: false, replace: true, preserveScroll: true }
|
{ 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 <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
|
||||||
|
}
|
||||||
|
if (filters.sort_order === "asc") {
|
||||||
|
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
}
|
||||||
|
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout
|
<AuthenticatedLayout
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
@@ -113,7 +145,15 @@ export default function ActivityLogIndex({ activities, filters }: Props) {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader className="bg-gray-50">
|
<TableHeader className="bg-gray-50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[180px]">時間</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
|
<TableHead className="w-[180px]">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('created_at')}
|
||||||
|
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
時間 <SortIcon field="created_at" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="w-[150px]">操作人員</TableHead>
|
<TableHead className="w-[150px]">操作人員</TableHead>
|
||||||
<TableHead className="w-[100px] text-center">動作</TableHead>
|
<TableHead className="w-[100px] text-center">動作</TableHead>
|
||||||
<TableHead className="w-[150px]">對象</TableHead>
|
<TableHead className="w-[150px]">對象</TableHead>
|
||||||
@@ -123,8 +163,11 @@ export default function ActivityLogIndex({ activities, filters }: Props) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{activities.data.length > 0 ? (
|
{activities.data.length > 0 ? (
|
||||||
activities.data.map((activity) => (
|
activities.data.map((activity, index) => (
|
||||||
<TableRow key={activity.id}>
|
<TableRow key={activity.id}>
|
||||||
|
<TableCell className="text-gray-500 font-medium text-center">
|
||||||
|
{activities.from + index}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-gray-500 font-medium whitespace-nowrap">
|
<TableCell className="text-gray-500 font-medium whitespace-nowrap">
|
||||||
{activity.created_at}
|
{activity.created_at}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -154,7 +197,8 @@ export default function ActivityLogIndex({ activities, filters }: Props) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleViewDetail(activity)}
|
onClick={() => handleViewDetail(activity)}
|
||||||
className="button-outlined-info"
|
className="button-outlined-primary"
|
||||||
|
title="檢視詳情"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -163,7 +207,7 @@ export default function ActivityLogIndex({ activities, filters }: Props) {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center py-8 text-gray-500">
|
<TableCell colSpan={7} className="text-center py-8 text-gray-500">
|
||||||
尚無操作紀錄
|
尚無操作紀錄
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, Link, router } from '@inertiajs/react';
|
||||||
import { cn } from "@/lib/utils";
|
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 { Button } from '@/Components/ui/button';
|
||||||
import { Badge } from '@/Components/ui/badge';
|
import { Badge } from '@/Components/ui/badge';
|
||||||
import {
|
import {
|
||||||
@@ -42,9 +42,13 @@ interface Role {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
roles: Role[];
|
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<Role | null>(null);
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||||
|
|
||||||
const handleDelete = (id: number, name: string) => {
|
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 <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
|
||||||
|
}
|
||||||
|
if (filters.sort_order === "asc") {
|
||||||
|
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
}
|
||||||
|
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticatedLayout
|
<AuthenticatedLayout
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
@@ -92,9 +126,30 @@ export default function RoleIndex({ roles }: Props) {
|
|||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead>名稱</TableHead>
|
<TableHead>名稱</TableHead>
|
||||||
<TableHead>代號</TableHead>
|
<TableHead>代號</TableHead>
|
||||||
<TableHead className="text-center">權限數量</TableHead>
|
<TableHead className="text-center">
|
||||||
<TableHead className="text-center">使用者人數</TableHead>
|
<button
|
||||||
<TableHead className="text-left">建立時間</TableHead>
|
onClick={() => handleSort('permissions_count')}
|
||||||
|
className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
權限數量 <SortIcon field="permissions_count" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('users_count')}
|
||||||
|
className="flex items-center justify-center gap-1 w-full hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
使用者人數 <SortIcon field="users_count" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('created_at')}
|
||||||
|
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
建立時間 <SortIcon field="created_at" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="text-center">操作</TableHead>
|
<TableHead className="text-center">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||||
import { Head, Link, router } from '@inertiajs/react';
|
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 { Button } from '@/Components/ui/button';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -47,6 +47,8 @@ interface Props {
|
|||||||
};
|
};
|
||||||
filters: {
|
filters: {
|
||||||
per_page?: string;
|
per_page?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,11 +68,41 @@ export default function UserIndex({ users, filters }: Props) {
|
|||||||
setPerPage(value);
|
setPerPage(value);
|
||||||
router.get(
|
router.get(
|
||||||
route('users.index'),
|
route('users.index'),
|
||||||
{ per_page: value },
|
{ ...filters, per_page: value },
|
||||||
{ preserveState: false, replace: true, preserveScroll: true }
|
{ 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 <ArrowUpDown className="h-4 w-4 text-muted-foreground ml-1" />;
|
||||||
|
}
|
||||||
|
if (filters.sort_order === "asc") {
|
||||||
|
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
}
|
||||||
|
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,9 +140,23 @@ export default function UserIndex({ users, filters }: Props) {
|
|||||||
<TableHeader className="bg-gray-50">
|
<TableHeader className="bg-gray-50">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||||
<TableHead className="w-[250px]">使用者</TableHead>
|
<TableHead>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('name')}
|
||||||
|
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
使用者 <SortIcon field="name" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
<TableHead>角色</TableHead>
|
<TableHead>角色</TableHead>
|
||||||
<TableHead className="w-[200px]">加入時間</TableHead>
|
<TableHead>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('created_at')}
|
||||||
|
className="flex items-center gap-1 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
加入時間 <SortIcon field="created_at" />
|
||||||
|
</button>
|
||||||
|
</TableHead>
|
||||||
<TableHead className="text-center">操作</TableHead>
|
<TableHead className="text-center">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user