813 lines
34 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import DashboardLayout from '../../components/Layout/DashboardLayout';
import {
DocumentDuplicateIcon,
MagnifyingGlassIcon,
PlusIcon,
EyeIcon,
PencilIcon,
TrashIcon,
ArrowDownTrayIcon,
FolderIcon,
DocumentTextIcon,
PhotoIcon,
FilmIcon,
MusicalNoteIcon,
ArchiveBoxIcon,
ChevronLeftIcon,
ChevronRightIcon,
CalendarIcon,
UserIcon,
TagIcon,
StarIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon
} from '@heroicons/react/24/outline';
interface Document {
id: string;
name: string;
originalName: string;
type: 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other';
category: 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other';
size: number;
uploadedBy: string;
uploadedAt: string;
lastModified: string;
status: 'active' | 'archived' | 'deleted';
tags: string[];
description?: string;
version: string;
downloadCount: number;
isPublic: boolean;
language?: string;
relatedOrderId?: string;
thumbnailUrl?: string;
url: string;
}
export default function Documents() {
const [documents, setDocuments] = useState<Document[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState<'all' | 'pdf' | 'word' | 'excel' | 'ppt' | 'image' | 'video' | 'audio' | 'text' | 'other'>('all');
const [filterCategory, setFilterCategory] = useState<'all' | 'contract' | 'translation' | 'template' | 'manual' | 'certificate' | 'report' | 'other'>('all');
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'archived' | 'deleted'>('all');
const [currentPage, setCurrentPage] = useState(1);
const [selectedDocuments, setSelectedDocuments] = useState<string[]>([]);
const [viewMode, setViewMode] = useState<'list' | 'grid'>('list');
const documentsPerPage = 12;
useEffect(() => {
loadDocuments();
}, [searchTerm, filterType, filterCategory, filterStatus, currentPage]);
const loadDocuments = async () => {
try {
setLoading(true);
// 模拟API调用
setTimeout(() => {
const mockData: Document[] = [
{
id: '1',
name: '服务合同模板-2024版',
originalName: 'service_contract_template_2024.pdf',
type: 'pdf',
category: 'contract',
size: 2048576, // 2MB
uploadedBy: '管理员',
uploadedAt: '2024-01-15 10:30',
lastModified: '2024-01-20 14:45',
status: 'active',
tags: ['合同', '模板', '服务'],
description: '标准服务合同模板,适用于翻译服务业务',
version: '2.1',
downloadCount: 156,
isPublic: true,
language: '中文',
url: '/documents/service_contract_template_2024.pdf'
},
{
id: '2',
name: '翻译质量评估报告',
originalName: 'quality_assessment_report.docx',
type: 'word',
category: 'report',
size: 1536000, // 1.5MB
uploadedBy: '张经理',
uploadedAt: '2024-01-18 16:20',
lastModified: '2024-01-19 09:15',
status: 'active',
tags: ['质量', '评估', '报告'],
description: '2024年第一季度翻译质量评估报告',
version: '1.0',
downloadCount: 89,
isPublic: false,
language: '中文',
relatedOrderId: 'ORD-2024-001',
url: '/documents/quality_assessment_report.docx'
},
{
id: '3',
name: '翻译员认证证书',
originalName: 'translator_certificate.jpg',
type: 'image',
category: 'certificate',
size: 512000, // 512KB
uploadedBy: '李翻译',
uploadedAt: '2024-01-10 11:45',
lastModified: '2024-01-10 11:45',
status: 'active',
tags: ['证书', '认证', '翻译员'],
description: '专业翻译员资格认证证书',
version: '1.0',
downloadCount: 23,
isPublic: false,
thumbnailUrl: '/thumbnails/translator_certificate_thumb.jpg',
url: '/documents/translator_certificate.jpg'
},
{
id: '4',
name: '用户操作手册',
originalName: 'user_manual_v3.pdf',
type: 'pdf',
category: 'manual',
size: 3072000, // 3MB
uploadedBy: '产品经理',
uploadedAt: '2024-01-12 13:30',
lastModified: '2024-01-16 10:20',
status: 'active',
tags: ['手册', '用户指南', '操作'],
description: '系统用户操作手册第三版',
version: '3.0',
downloadCount: 234,
isPublic: true,
language: '中文',
url: '/documents/user_manual_v3.pdf'
},
{
id: '5',
name: '翻译项目演示视频',
originalName: 'project_demo.mp4',
type: 'video',
category: 'other',
size: 15728640, // 15MB
uploadedBy: '市场部',
uploadedAt: '2024-01-08 14:15',
lastModified: '2024-01-08 14:15',
status: 'active',
tags: ['演示', '视频', '项目'],
description: '翻译项目流程演示视频',
version: '1.0',
downloadCount: 67,
isPublic: true,
thumbnailUrl: '/thumbnails/project_demo_thumb.jpg',
url: '/documents/project_demo.mp4'
},
{
id: '6',
name: '财务报表模板',
originalName: 'financial_template.xlsx',
type: 'excel',
category: 'template',
size: 1024000, // 1MB
uploadedBy: '财务部',
uploadedAt: '2024-01-05 09:45',
lastModified: '2024-01-14 16:30',
status: 'archived',
tags: ['财务', '模板', '报表'],
description: '月度财务报表模板',
version: '2.5',
downloadCount: 45,
isPublic: false,
url: '/documents/financial_template.xlsx'
},
{
id: '7',
name: '产品介绍PPT',
originalName: 'product_introduction.pptx',
type: 'ppt',
category: 'other',
size: 5120000, // 5MB
uploadedBy: '销售部',
uploadedAt: '2024-01-03 15:20',
lastModified: '2024-01-11 11:10',
status: 'active',
tags: ['产品', '介绍', '演示'],
description: '公司产品介绍演示文稿',
version: '1.3',
downloadCount: 112,
isPublic: true,
language: '中文',
url: '/documents/product_introduction.pptx'
},
{
id: '8',
name: '客户反馈音频',
originalName: 'customer_feedback.mp3',
type: 'audio',
category: 'other',
size: 2560000, // 2.5MB
uploadedBy: '客服部',
uploadedAt: '2024-01-01 10:00',
lastModified: '2024-01-01 10:00',
status: 'active',
tags: ['客户', '反馈', '音频'],
description: '客户服务质量反馈录音',
version: '1.0',
downloadCount: 34,
isPublic: false,
url: '/documents/customer_feedback.mp3'
}
];
// 应用过滤器
let filteredData = mockData;
if (searchTerm) {
const term = searchTerm.toLowerCase();
filteredData = filteredData.filter(doc =>
doc.name.toLowerCase().includes(term) ||
doc.originalName.toLowerCase().includes(term) ||
doc.description?.toLowerCase().includes(term) ||
doc.tags.some(tag => tag.toLowerCase().includes(term))
);
}
if (filterType !== 'all') {
filteredData = filteredData.filter(doc => doc.type === filterType);
}
if (filterCategory !== 'all') {
filteredData = filteredData.filter(doc => doc.category === filterCategory);
}
if (filterStatus !== 'all') {
filteredData = filteredData.filter(doc => doc.status === filterStatus);
}
setDocuments(filteredData);
setLoading(false);
}, 1000);
} catch (error) {
console.error('加载文档数据失败:', error);
setLoading(false);
}
};
const handleSelectDocument = (documentId: string) => {
setSelectedDocuments(prev =>
prev.includes(documentId)
? prev.filter(id => id !== documentId)
: [...prev, documentId]
);
};
const handleSelectAll = () => {
if (selectedDocuments.length === documents.length) {
setSelectedDocuments([]);
} else {
setSelectedDocuments(documents.map(doc => doc.id));
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'pdf':
case 'word':
case 'text':
return <DocumentTextIcon className="h-5 w-5" />;
case 'excel':
return <DocumentDuplicateIcon className="h-5 w-5" />;
case 'ppt':
return <DocumentDuplicateIcon className="h-5 w-5" />;
case 'image':
return <PhotoIcon className="h-5 w-5" />;
case 'video':
return <FilmIcon className="h-5 w-5" />;
case 'audio':
return <MusicalNoteIcon className="h-5 w-5" />;
default:
return <DocumentDuplicateIcon className="h-5 w-5" />;
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'pdf': return 'text-red-600';
case 'word': return 'text-blue-600';
case 'excel': return 'text-green-600';
case 'ppt': return 'text-orange-600';
case 'image': return 'text-purple-600';
case 'video': return 'text-pink-600';
case 'audio': return 'text-indigo-600';
case 'text': return 'text-gray-600';
default: return 'text-gray-600';
}
};
const getCategoryText = (category: string) => {
switch (category) {
case 'contract': return '合同';
case 'translation': return '翻译';
case 'template': return '模板';
case 'manual': return '手册';
case 'certificate': return '证书';
case 'report': return '报告';
case 'other': return '其他';
default: return category;
}
};
const getCategoryColor = (category: string) => {
switch (category) {
case 'contract': return 'bg-red-100 text-red-800';
case 'translation': return 'bg-blue-100 text-blue-800';
case 'template': return 'bg-green-100 text-green-800';
case 'manual': return 'bg-yellow-100 text-yellow-800';
case 'certificate': return 'bg-purple-100 text-purple-800';
case 'report': return 'bg-indigo-100 text-indigo-800';
case 'other': return 'bg-gray-100 text-gray-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active': return '正常';
case 'archived': return '已归档';
case 'deleted': return '已删除';
default: return status;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'bg-green-100 text-green-800';
case 'archived': return 'bg-yellow-100 text-yellow-800';
case 'deleted': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const totalPages = Math.ceil(documents.length / documentsPerPage);
const currentDocuments = documents.slice(
(currentPage - 1) * documentsPerPage,
currentPage * documentsPerPage
);
if (loading) {
return (
<DashboardLayout title="文档管理">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<div className="mt-4 text-lg text-gray-600">...</div>
</div>
</div>
</DashboardLayout>
);
}
return (
<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-1 text-sm text-gray-600">
</p>
</div>
<div className="mt-4 sm:mt-0 flex space-x-3">
<button
type="button"
onClick={() => setViewMode(viewMode === 'list' ? 'grid' : 'list')}
className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{viewMode === 'list' ? '网格视图' : '列表视图'}
</button>
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<PlusIcon className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<DocumentDuplicateIcon className="h-6 w-6 text-gray-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">{documents.length}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<CheckCircleIcon className="h-6 w-6 text-green-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{documents.filter(d => d.status === 'active').length}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<ArchiveBoxIcon className="h-6 w-6 text-yellow-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{documents.filter(d => d.status === 'archived').length}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<ArrowDownTrayIcon className="h-6 w-6 text-blue-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{documents.reduce((sum, d) => sum + d.downloadCount, 0)}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{/* 搜索和过滤 */}
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
{/* 搜索框 */}
<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"
placeholder="搜索文档名称、描述或标签..."
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-indigo-500 focus:border-indigo-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* 文件类型过滤 */}
<div className="relative">
<select
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
value={filterType}
onChange={(e) => setFilterType(e.target.value as any)}
>
<option value="all"></option>
<option value="pdf">PDF</option>
<option value="word">Word</option>
<option value="excel">Excel</option>
<option value="ppt">PPT</option>
<option value="image"></option>
<option value="video"></option>
<option value="audio"></option>
<option value="text"></option>
<option value="other"></option>
</select>
</div>
{/* 分类过滤 */}
<div className="relative">
<select
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value as any)}
>
<option value="all"></option>
<option value="contract"></option>
<option value="translation"></option>
<option value="template"></option>
<option value="manual"></option>
<option value="certificate"></option>
<option value="report"></option>
<option value="other"></option>
</select>
</div>
{/* 状态过滤 */}
<div className="relative">
<select
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as any)}
>
<option value="all"></option>
<option value="active"></option>
<option value="archived"></option>
<option value="deleted"></option>
</select>
</div>
</div>
{/* 批量操作 */}
{selectedDocuments.length > 0 && (
<div className="mt-4 flex items-center justify-between bg-gray-50 p-3 rounded-md">
<span className="text-sm text-gray-700">
{selectedDocuments.length}
</span>
<div className="flex space-x-2">
<button 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
</button>
<button 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500">
</button>
<button 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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
</button>
</div>
</div>
)}
</div>
{/* 文档列表/网格 */}
{viewMode === 'list' ? (
<div className="bg-white shadow rounded-lg overflow-hidden">
<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-indigo-600 focus:ring-indigo-500"
checked={documents.length > 0 && selectedDocuments.length === documents.length}
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">
{currentDocuments.map((document) => (
<tr key={document.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-indigo-600 focus:ring-indigo-500"
checked={selectedDocuments.includes(document.id)}
onChange={() => handleSelectDocument(document.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className={`flex-shrink-0 ${getTypeColor(document.type)}`}>
{getTypeIcon(document.type)}
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{document.name}</div>
<div className="text-sm text-gray-500">{document.originalName}</div>
{document.description && (
<div className="text-xs text-gray-400 mt-1">{document.description}</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getCategoryColor(document.category)}`}>
{getCategoryText(document.category)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatFileSize(document.size)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="flex items-center text-sm text-gray-900">
<UserIcon className="h-4 w-4 mr-1" />
{document.uploadedBy}
</div>
<div className="flex items-center text-sm text-gray-500 mt-1">
<CalendarIcon className="h-4 w-4 mr-1" />
{document.uploadedAt}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(document.status)}`}>
{getStatusText(document.status)}
</span>
{document.isPublic && (
<div className="text-xs text-blue-500 mt-1"></div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{document.downloadCount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
<button className="text-indigo-600 hover:text-indigo-900" title="预览">
<EyeIcon className="h-4 w-4" />
</button>
<button className="text-blue-600 hover:text-blue-900" title="下载">
<ArrowDownTrayIcon className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900" title="编辑">
<PencilIcon className="h-4 w-4" />
</button>
<button className="text-red-600 hover:text-red-900" title="删除">
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{currentDocuments.map((document) => (
<div key={document.id} className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between mb-3">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
checked={selectedDocuments.includes(document.id)}
onChange={() => handleSelectDocument(document.id)}
/>
<div className="flex space-x-1">
<button className="text-indigo-600 hover:text-indigo-900" title="预览">
<EyeIcon className="h-4 w-4" />
</button>
<button className="text-blue-600 hover:text-blue-900" title="下载">
<ArrowDownTrayIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="text-center mb-4">
{document.thumbnailUrl ? (
<img
src={document.thumbnailUrl}
alt={document.name}
className="w-16 h-16 mx-auto object-cover rounded"
/>
) : (
<div className={`w-16 h-16 mx-auto flex items-center justify-center rounded bg-gray-100 ${getTypeColor(document.type)}`}>
{getTypeIcon(document.type)}
</div>
)}
</div>
<div className="text-center">
<h3 className="text-sm font-medium text-gray-900 mb-1">{document.name}</h3>
<p className="text-xs text-gray-500 mb-2">{formatFileSize(document.size)}</p>
<div className="flex justify-center mb-2">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getCategoryColor(document.category)}`}>
{getCategoryText(document.category)}
</span>
</div>
<div className="text-xs text-gray-500">
<div>{document.uploadedBy}</div>
<div>{document.uploadedAt}</div>
<div> {document.downloadCount} </div>
</div>
</div>
</div>
))}
</div>
</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(Math.max(1, currentPage - 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(Math.min(totalPages, currentPage + 1))}
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) * documentsPerPage + 1}</span> {' '}
<span className="font-medium">{Math.min(currentPage * documentsPerPage, documents.length)}</span>
<span className="font-medium">{documents.length}</span>
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 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"
>
<ChevronLeftIcon className="h-5 w-5" />
</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-indigo-50 border-indigo-500 text-indigo-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
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"
>
<ChevronRightIcon className="h-5 w-5" />
</button>
</nav>
</div>
</div>
</div>
)}
</div>
</DashboardLayout>
);
}