style: 統一所有表格標題樣式為一般粗細並修正排序功能
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 56s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-19 09:30:02 +08:00
parent 0d7bb2758d
commit 74417e2e31
12 changed files with 311 additions and 62 deletions

View File

@@ -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. 分頁規範

View File

@@ -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'),
], ],
]); ]);
} }

View File

@@ -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']),
]); ]);
} }

View File

@@ -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']),
]); ]);
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>