mars f20988b90c feat: 完成所有页面的演示模式实现
- 更新 DashboardLayout 组件,统一使用演示模式布局
- 实现仪表盘页面的完整演示数据和功能
- 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能
- 实现通话记录页面的演示数据和录音播放功能
- 完成翻译员管理页面的演示模式
- 实现订单管理页面的完整功能
- 完成发票管理页面的演示数据
- 更新文档管理页面
- 添加 utils.ts 工具函数库
- 完善 API 路由和数据库结构
- 修复各种 TypeScript 类型错误
- 统一界面风格和用户体验
2025-06-30 19:42:43 +08:00

796 lines
34 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';
import DashboardLayout from '../../components/Layout/DashboardLayout';
import { toast } from 'react-hot-toast';
import {
MagnifyingGlassIcon,
PlusIcon,
EyeIcon,
PencilIcon,
TrashIcon,
DocumentTextIcon,
ArrowDownTrayIcon,
PrinterIcon,
CurrencyDollarIcon,
CalendarIcon,
CheckCircleIcon,
XCircleIcon,
ClockIcon,
ExclamationTriangleIcon,
UserIcon,
BuildingOfficeIcon,
EnvelopeIcon,
PhoneIcon
} from '@heroicons/react/24/outline';
import { getDemoData } from '../../lib/demo-data';
import { formatTime } from '../../lib/utils';
interface Invoice {
id: string;
invoice_number: string;
order_id: string;
order_number: string;
user_name: string;
user_email: string;
user_company?: string;
user_phone?: string;
interpreter_name: string;
service_type: string;
service_description: string;
amount: number;
tax_amount: number;
total_amount: number;
status: 'draft' | 'sent' | 'paid' | 'overdue' | 'cancelled';
issue_date: string;
due_date: string;
paid_date?: string;
notes?: string;
created_at: string;
updated_at: string;
}
interface InvoiceFilters {
search: string;
status: string;
date_range: string;
amount_range: string;
}
export default function Invoices() {
const router = useRouter();
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [loading, setLoading] = useState(true);
const [selectedInvoices, setSelectedInvoices] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const [filters, setFilters] = useState<InvoiceFilters>({
search: '',
status: '',
date_range: '',
amount_range: ''
});
const pageSize = 10;
useEffect(() => {
fetchInvoices();
}, [currentPage, filters]);
const fetchInvoices = async () => {
try {
setLoading(true);
// 模拟加载延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 使用演示数据
const mockInvoices: Invoice[] = [
{
id: '1',
invoice_number: 'INV-2024-001',
order_id: '1',
order_number: 'ORD-2024-001',
user_name: '张先生',
user_email: 'zhang@example.com',
user_company: '北京科技有限公司',
user_phone: '13800138001',
interpreter_name: '王翻译',
service_type: '视频通话翻译',
service_description: '商务会议翻译服务(中英互译)',
amount: 450,
tax_amount: 27,
total_amount: 477,
status: 'paid',
issue_date: '2024-01-20T00:00:00Z',
due_date: '2024-02-19T00:00:00Z',
paid_date: '2024-01-25T00:00:00Z',
notes: '已按时完成支付',
created_at: '2024-01-20T10:00:00Z',
updated_at: '2024-01-25T14:30:00Z'
},
{
id: '2',
invoice_number: 'INV-2024-002',
order_id: '2',
order_number: 'ORD-2024-002',
user_name: '李女士',
user_email: 'li@example.com',
user_company: '上海医疗中心',
user_phone: '13800138002',
interpreter_name: '陈口译',
service_type: '语音通话翻译',
service_description: '医疗咨询翻译服务(中日互译)',
amount: 300,
tax_amount: 18,
total_amount: 318,
status: 'sent',
issue_date: '2024-01-21T00:00:00Z',
due_date: '2024-02-20T00:00:00Z',
notes: '等待客户支付',
created_at: '2024-01-21T09:00:00Z',
updated_at: '2024-01-21T09:00:00Z'
},
{
id: '3',
invoice_number: 'INV-2024-003',
order_id: '3',
order_number: 'ORD-2024-003',
user_name: '王总',
user_email: 'wangzong@example.com',
user_company: '深圳国际贸易公司',
user_phone: '13800138003',
interpreter_name: '刘同传',
service_type: '现场翻译',
service_description: '大型会议同声传译服务(中英互译)',
amount: 1800,
tax_amount: 108,
total_amount: 1908,
status: 'draft',
issue_date: '2024-01-22T00:00:00Z',
due_date: '2024-02-21T00:00:00Z',
notes: '待客户确认后发送',
created_at: '2024-01-22T16:00:00Z',
updated_at: '2024-01-22T16:00:00Z'
},
{
id: '4',
invoice_number: 'INV-2024-004',
order_id: '4',
order_number: 'ORD-2024-004',
user_name: '赵经理',
user_email: 'zhao@example.com',
user_company: '广州制造业集团',
user_phone: '13800138004',
interpreter_name: '李专家',
service_type: '视频通话翻译',
service_description: '技术交流翻译服务(中韩互译)',
amount: 200,
tax_amount: 12,
total_amount: 212,
status: 'cancelled',
issue_date: '2024-01-20T00:00:00Z',
due_date: '2024-02-19T00:00:00Z',
notes: '订单取消,发票作废',
created_at: '2024-01-20T14:00:00Z',
updated_at: '2024-01-20T16:00:00Z'
},
{
id: '5',
invoice_number: 'INV-2024-005',
order_id: '5',
order_number: 'ORD-2024-005',
user_name: '孙先生',
user_email: 'sun@example.com',
user_company: '杭州技术服务公司',
user_phone: '13800138005',
interpreter_name: '张语言',
service_type: '语音通话翻译',
service_description: '技术文档咨询翻译服务(中德互译)',
amount: 250,
tax_amount: 15,
total_amount: 265,
status: 'overdue',
issue_date: '2024-01-15T00:00:00Z',
due_date: '2024-01-30T00:00:00Z',
notes: '已逾期,需要催收',
created_at: '2024-01-15T13:00:00Z',
updated_at: '2024-01-31T10:00:00Z'
},
{
id: '6',
invoice_number: 'INV-2024-006',
order_id: '6',
order_number: 'ORD-2024-006',
user_name: '周女士',
user_email: 'zhou@example.com',
user_company: '成都教育机构',
user_phone: '13800138006',
interpreter_name: '赵技术',
service_type: '视频通话翻译',
service_description: '学术会议翻译服务(中英互译)',
amount: 200,
tax_amount: 12,
total_amount: 212,
status: 'sent',
issue_date: '2024-01-19T00:00:00Z',
due_date: '2024-02-18T00:00:00Z',
notes: '已发送给客户',
created_at: '2024-01-19T11:00:00Z',
updated_at: '2024-01-19T11:00:00Z'
}
];
// 应用过滤器
let filteredInvoices = mockInvoices;
if (filters.search) {
filteredInvoices = filteredInvoices.filter(invoice =>
invoice.invoice_number.toLowerCase().includes(filters.search.toLowerCase()) ||
invoice.order_number.toLowerCase().includes(filters.search.toLowerCase()) ||
invoice.user_name.toLowerCase().includes(filters.search.toLowerCase()) ||
invoice.user_email.toLowerCase().includes(filters.search.toLowerCase()) ||
invoice.user_company?.toLowerCase().includes(filters.search.toLowerCase()) ||
invoice.interpreter_name.toLowerCase().includes(filters.search.toLowerCase())
);
}
if (filters.status) {
filteredInvoices = filteredInvoices.filter(invoice => invoice.status === filters.status);
}
// 分页
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedInvoices = filteredInvoices.slice(startIndex, endIndex);
setInvoices(paginatedInvoices);
setTotalCount(filteredInvoices.length);
setTotalPages(Math.ceil(filteredInvoices.length / pageSize));
} catch (error) {
console.error('Failed to fetch invoices:', error);
toast.error('加载发票失败');
} finally {
setLoading(false);
}
};
const handleSearch = (value: string) => {
setFilters(prev => ({ ...prev, search: value }));
setCurrentPage(1);
};
const handleFilterChange = (key: keyof InvoiceFilters, value: string) => {
setFilters(prev => ({ ...prev, [key]: value }));
setCurrentPage(1);
};
const handleSelectInvoice = (invoiceId: string) => {
setSelectedInvoices(prev =>
prev.includes(invoiceId)
? prev.filter(id => id !== invoiceId)
: [...prev, invoiceId]
);
};
const handleSelectAll = () => {
if (selectedInvoices.length === invoices.length) {
setSelectedInvoices([]);
} else {
setSelectedInvoices(invoices.map(invoice => invoice.id));
}
};
const handleBulkAction = async (action: 'send' | 'mark_paid' | 'cancel' | 'delete') => {
if (selectedInvoices.length === 0) {
toast.error('请选择要操作的发票');
return;
}
try {
const actionText = action === 'send' ? '发送' : action === 'mark_paid' ? '标记已付' : action === 'cancel' ? '取消' : '删除';
toast.loading(`正在${actionText}发票...`, { id: 'bulk-action' });
// 模拟操作延迟
await new Promise(resolve => setTimeout(resolve, 1500));
toast.success(`成功${actionText} ${selectedInvoices.length} 张发票`, { id: 'bulk-action' });
setSelectedInvoices([]);
fetchInvoices();
} catch (error) {
toast.error('操作失败', { id: 'bulk-action' });
}
};
const handleExport = async () => {
try {
toast.loading('正在导出发票数据...', { id: 'export' });
// 模拟导出延迟
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success('发票数据导出成功', { id: 'export' });
} catch (error) {
toast.error('导出失败', { id: 'export' });
}
};
const handlePrint = async (invoiceId: string) => {
try {
toast.loading('正在准备打印...', { id: 'print' });
// 模拟打印准备延迟
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('发票已发送到打印机', { id: 'print' });
} catch (error) {
toast.error('打印失败', { id: 'print' });
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'paid':
return 'text-green-800 bg-green-100';
case 'sent':
return 'text-blue-800 bg-blue-100';
case 'draft':
return 'text-gray-800 bg-gray-100';
case 'overdue':
return 'text-red-800 bg-red-100';
case 'cancelled':
return 'text-red-800 bg-red-100';
default:
return 'text-gray-800 bg-gray-100';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'paid':
return '已支付';
case 'sent':
return '已发送';
case 'draft':
return '草稿';
case 'overdue':
return '逾期';
case 'cancelled':
return '已取消';
default:
return status;
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'paid':
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
case 'sent':
return <DocumentTextIcon className="h-4 w-4 text-blue-500" />;
case 'draft':
return <PencilIcon className="h-4 w-4 text-gray-500" />;
case 'overdue':
return <ExclamationTriangleIcon className="h-4 w-4 text-red-500" />;
case 'cancelled':
return <XCircleIcon className="h-4 w-4 text-red-500" />;
default:
return <ClockIcon className="h-4 w-4 text-gray-500" />;
}
};
const formatCurrency = (amount: number) => {
return `¥${amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN');
};
return (
<>
<Head>
<title> - </title>
</Head>
<DashboardLayout title="发票管理">
<div className="space-y-6">
{/* 页面标题和操作 */}
<div className="sm:flex sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="mt-2 text-sm text-gray-700">
</p>
</div>
<div className="mt-4 sm:mt-0 sm:flex sm:space-x-3">
<button
onClick={handleExport}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<ArrowDownTrayIcon className="h-4 w-4 mr-2" />
</button>
<button
onClick={() => router.push('/dashboard/invoices/new')}
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<PlusIcon className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* 搜索和过滤器 */}
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label htmlFor="search" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
id="search"
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
placeholder="搜索发票号、订单号、客户..."
value={filters.search}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</div>
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="status"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.status}
onChange={(e) => handleFilterChange('status', e.target.value)}
>
<option value=""></option>
<option value="draft">稿</option>
<option value="sent"></option>
<option value="paid"></option>
<option value="overdue"></option>
<option value="cancelled"></option>
</select>
</div>
<div>
<label htmlFor="date_range" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="date_range"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.date_range}
onChange={(e) => handleFilterChange('date_range', e.target.value)}
>
<option value=""></option>
<option value="today"></option>
<option value="week"></option>
<option value="month"></option>
<option value="quarter"></option>
</select>
</div>
<div>
<label htmlFor="amount_range" className="block text-sm font-medium text-gray-700 mb-1">
</label>
<select
id="amount_range"
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
value={filters.amount_range}
onChange={(e) => handleFilterChange('amount_range', e.target.value)}
>
<option value=""></option>
<option value="0-500">¥0 - ¥500</option>
<option value="500-1000">¥500 - ¥1,000</option>
<option value="1000-5000">¥1,000 - ¥5,000</option>
<option value="5000+">¥5,000+</option>
</select>
</div>
</div>
</div>
{/* 批量操作 */}
{selectedInvoices.length > 0 && (
<div className="bg-white shadow rounded-lg p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-700">
{selectedInvoices.length}
</span>
<div className="flex space-x-2">
<button
onClick={() => handleBulkAction('send')}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
>
</button>
<button
onClick={() => handleBulkAction('mark_paid')}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
>
</button>
<button
onClick={() => handleBulkAction('cancel')}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700"
>
</button>
<button
onClick={() => handleBulkAction('delete')}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
>
</button>
</div>
</div>
</div>
)}
{/* 发票列表 */}
<div className="bg-white shadow rounded-lg overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="relative px-6 py-3">
<input
type="checkbox"
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
checked={selectedInvoices.length === invoices.length && invoices.length > 0}
onChange={handleSelectAll}
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only"></span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{invoices.map((invoice) => (
<tr key={invoice.id} className="hover:bg-gray-50">
<td className="relative px-6 py-4">
<input
type="checkbox"
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
checked={selectedInvoices.includes(invoice.id)}
onChange={() => handleSelectInvoice(invoice.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{invoice.invoice_number}
</div>
<div className="text-sm text-gray-500">
: {invoice.order_number}
</div>
<div className="text-sm text-gray-500">
{formatTime(invoice.created_at)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm font-medium text-gray-900">
<UserIcon className="h-4 w-4 mr-1 text-gray-400" />
{invoice.user_name}
</div>
<div className="flex items-center text-sm text-gray-500">
<EnvelopeIcon className="h-4 w-4 mr-1 text-gray-400" />
{invoice.user_email}
</div>
{invoice.user_company && (
<div className="flex items-center text-sm text-gray-500">
<BuildingOfficeIcon className="h-4 w-4 mr-1 text-gray-400" />
{invoice.user_company}
</div>
)}
{invoice.user_phone && (
<div className="flex items-center text-sm text-gray-500">
<PhoneIcon className="h-4 w-4 mr-1 text-gray-400" />
{invoice.user_phone}
</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{invoice.service_type}
</div>
<div className="text-sm text-gray-500">
: {invoice.interpreter_name}
</div>
<div className="text-sm text-gray-500 max-w-48 truncate" title={invoice.service_description}>
{invoice.service_description}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{formatCurrency(invoice.total_amount)}
</div>
<div className="text-sm text-gray-500">
: {formatCurrency(invoice.amount)}
</div>
<div className="text-sm text-gray-500">
: {formatCurrency(invoice.tax_amount)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-900">
<CalendarIcon className="h-4 w-4 mr-1 text-gray-400" />
: {formatDate(invoice.issue_date)}
</div>
<div className="flex items-center text-sm text-gray-500">
<ClockIcon className="h-4 w-4 mr-1 text-gray-400" />
: {formatDate(invoice.due_date)}
</div>
{invoice.paid_date && (
<div className="flex items-center text-sm text-green-600">
<CheckCircleIcon className="h-4 w-4 mr-1" />
: {formatDate(invoice.paid_date)}
</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getStatusIcon(invoice.status)}
<span className={`ml-2 inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(invoice.status)}`}>
{getStatusText(invoice.status)}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
<button
onClick={() => router.push(`/dashboard/invoices/${invoice.id}`)}
className="text-blue-600 hover:text-blue-900"
title="查看详情"
>
<EyeIcon className="h-4 w-4" />
</button>
<button
onClick={() => handlePrint(invoice.id)}
className="text-purple-600 hover:text-purple-900"
title="打印发票"
>
<PrinterIcon className="h-4 w-4" />
</button>
<button
onClick={() => router.push(`/dashboard/invoices/${invoice.id}/edit`)}
className="text-green-600 hover:text-green-900"
title="编辑"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => {
if (confirm('确定要删除这张发票吗?')) {
toast.success('发票删除成功');
fetchInvoices();
}
}}
className="text-red-600 hover:text-red-900"
title="删除"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
<span className="font-medium">{(currentPage - 1) * pageSize + 1}</span> {' '}
<span className="font-medium">{Math.min(currentPage * pageSize, totalCount)}</span>
<span className="font-medium">{totalCount}</span>
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
page === currentPage
? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</nav>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
</DashboardLayout>
</>
);
}