diff --git a/frontend/src/app/documents/page.tsx b/frontend/src/app/documents/page.tsx index 1c7eda40..8f4eae72 100644 --- a/frontend/src/app/documents/page.tsx +++ b/frontend/src/app/documents/page.tsx @@ -4,14 +4,13 @@ import { AppShell } from "../../components/AppShell"; import { apiClient } from "@/lib/api"; import { useEffect, useMemo, useState } from "react"; import dynamic from "next/dynamic"; -import type { Document, DocumentStatus } from "@/types/api"; +import type { Document } from "@/types/api"; import { Search, FileText, Plus, Eye, X, - LayoutDashboard, FolderOpen, Tags, LayoutGrid, @@ -21,23 +20,18 @@ import { AlertCircle, Calendar, User, - File, - HardDrive, Users, Building2, Download, + Edit, + ChevronDown, } from "lucide-react"; -import { DocumentStatusBadge } from "@/components/documents/DocumentStatusBadge"; -import { DocumentWorkflowButtons } from "@/components/documents/DocumentWorkflowButtons"; -import { DocumentAcknowledgement } from "@/components/documents/DocumentAcknowledgement"; import { DocumentAcknowledgementsReport } from "@/components/documents/DocumentAcknowledgementsReport"; -import { DocumentComments } from "@/components/documents/DocumentComments"; -import { DocumentVersionHistory } from "@/components/documents/DocumentVersionHistory"; -import { DocumentRelated } from "@/components/documents/DocumentRelated"; +import { DocumentMetadataEditor } from "@/components/documents/DocumentMetadataEditor"; +import { DocumentDetailModal } from "@/components/documents/DocumentDetailModal"; import { FolderTree, type FolderNode } from "@/components/documents/folders"; -import { AdvancedSearch } from "@/components/documents/search"; import { BulkActionsToolbar, useDocumentSelection } from "@/components/documents/batch"; -import { DocumentsDashboard } from "@/components/documents/dashboard"; +import { TagManagementModal } from "@/components/documents/tags"; import { Modal } from "@/components/ui"; // Динамический импорт компонентов с PDF обработкой (избегаем SSR ошибок с DOMMatrix) @@ -67,31 +61,31 @@ function formatDate(value?: string): string { }); } -type ViewMode = "documents" | "dashboard"; - export default function DocumentsPage() { // State const [documents, setDocuments] = useState([]); const [folders, setFolders] = useState([]); const [selectedFolderId, setSelectedFolderId] = useState(null); const [search, setSearch] = useState(""); - const [statusFilter, setStatusFilter] = useState("all"); const [selectedTags, setSelectedTags] = useState([]); - const [selectedTypes, setSelectedTypes] = useState([]); const [availableTags, setAvailableTags] = useState>([]); - const [availableTypes, setAvailableTypes] = useState>([]); + const [dateFrom, setDateFrom] = useState(""); + const [dateTo, setDateTo] = useState(""); + const [sortBy, setSortBy] = useState<"date" | "title">("date"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + const [showFilters, setShowFilters] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [viewMode, setViewMode] = useState("documents"); // Modals const [showUploadForm, setShowUploadForm] = useState(false); - const [showAdvancedSearch, setShowAdvancedSearch] = useState(false); const [showCreateFolder, setShowCreateFolder] = useState(false); + const [showTagManagement, setShowTagManagement] = useState(false); const [showAcknowledgementsReport, setShowAcknowledgementsReport] = useState<{ documentId: number; documentTitle: string; } | null>(null); + const [showMetadataEditor, setShowMetadataEditor] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const [previewFile, setPreviewFile] = useState<{ url: string; name: string } | null>(null); const [pdfViewerFile, setPdfViewerFile] = useState<{ url: string; name: string } | null>(null); @@ -107,9 +101,6 @@ export default function DocumentsPage() { setLoading(true); setError(null); const params: any = {}; - if (statusFilter !== "all") { - params.status = statusFilter; - } if (selectedFolderId !== null) { params.folder_id = selectedFolderId; } @@ -143,34 +134,20 @@ export default function DocumentsPage() { } }; - const loadTypes = async () => { - try { - const response = await apiClient.getDocumentTypes(); - setAvailableTypes(response.results || response || []); - } catch (err) { - console.error("Ошибка загрузки типов:", err); - } - }; - useEffect(() => { loadDocuments(); - }, [statusFilter, selectedFolderId]); + }, [selectedFolderId]); useEffect(() => { loadFolders(); loadTags(); - loadTypes(); }, []); const filteredDocuments = useMemo(() => { const q = search.trim().toLowerCase(); - const sorted = [...documents].sort((a, b) => { - const aTime = new Date(a.created_at).getTime() || 0; - const bTime = new Date(b.created_at).getTime() || 0; - return bTime - aTime; - }); - - return sorted.filter((doc) => { + + // Filter documents + let filtered = documents.filter((doc) => { // Text search if (q) { const title = doc.title.toLowerCase(); @@ -190,17 +167,41 @@ export default function DocumentsPage() { if (!hasTag) return false; } - // Type filter - if (selectedTypes.length > 0) { - const docTypeId = doc.document_type?.id; - if (!docTypeId || !selectedTypes.includes(docTypeId)) { - return false; - } + // Date from filter + if (dateFrom) { + const docDate = new Date(doc.created_at); + const fromDate = new Date(dateFrom); + if (docDate < fromDate) return false; + } + + // Date to filter + if (dateTo) { + const docDate = new Date(doc.created_at); + const toDate = new Date(dateTo); + toDate.setHours(23, 59, 59, 999); // Include the entire day + if (docDate > toDate) return false; } return true; }); - }, [documents, search, selectedTags, selectedTypes]); + + // Sort documents + filtered.sort((a, b) => { + let compareValue = 0; + + if (sortBy === "date") { + const aTime = new Date(a.created_at).getTime() || 0; + const bTime = new Date(b.created_at).getTime() || 0; + compareValue = bTime - aTime; + } else if (sortBy === "title") { + compareValue = a.title.localeCompare(b.title, 'ru'); + } + + return sortOrder === "desc" ? compareValue : -compareValue; + }); + + return filtered; + }, [documents, search, selectedTags, dateFrom, dateTo, sortBy, sortOrder]); // Find selected folder and build breadcrumb path const selectedFolder = useMemo(() => { @@ -225,28 +226,6 @@ export default function DocumentsPage() { return selectedFolder.path.split(' / '); }, [selectedFolder]); - // Mock dashboard stats - const dashboardStats = useMemo(() => { - const byStatus = documents.reduce((acc: any[], doc) => { - const existing = acc.find((s) => s.status === doc.status); - if (existing) { - existing.count++; - } else { - acc.push({ status: doc.status, count: 1 }); - } - return acc; - }, []); - - return { - totalDocuments: documents.length, - documentsByType: [{ type: "Общие", count: documents.length }], - documentsByStatus: byStatus, - uploadsOverTime: [], - myDocuments: documents.filter((d) => d.created_by).length, - recentActivity: [], - }; - }, [documents]); - return (
@@ -255,32 +234,6 @@ export default function DocumentsPage() {
{/* Left Section: Navigation */}
- {/* View Mode Tabs */} -
- - -
- {/* Folder Filter */}
+
+ + {/* Active Filters Tags */} + {(selectedTags.length > 0 || dateFrom || dateTo) && ( +
+ {selectedTags.map(tagId => { + const tag = availableTags.find(t => t.id === tagId); + return tag ? ( + + + {tag.name} + + + ) : null; + })} + {dateFrom && ( + + + С: {new Date(dateFrom).toLocaleDateString('ru')} + + + )} + {dateTo && ( + + + До: {new Date(dateTo).toLocaleDateString('ru')} + + + )} + +
+ )} + + {/* Filters Panel */} + {showFilters && ( +
+ {/* Tags Filter */} +
+
+ + +
+ {availableTags.length > 0 ? ( +
+ {availableTags.map(tag => ( + + ))} +
+ ) : ( +

Нет доступных тегов

+ )}
- - - -
- - {/* Tags and Types filters */} -
- {/* Tags multi-select */} - {availableTags.length > 0 && ( -
- setDateFrom(e.target.value)} className="w-full rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" - size={3} - > - - {availableTags.map(tag => ( - - ))} - - {selectedTags.length > 0 && ( -
- {selectedTags.map(tagId => { - const tag = availableTags.find(t => t.id === tagId); - return tag ? ( - - {tag.name} - - - ) : null; - })} -
- )} + />
- )} - - {/* Types multi-select */} - {availableTypes.length > 0 && ( -
- setDateTo(e.target.value)} className="w-full rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" - size={3} - > - - {availableTypes.map(type => ( - - ))} - - {selectedTypes.length > 0 && ( -
- {selectedTypes.map(typeId => { - const type = availableTypes.find(t => t.id === typeId); - return type ? ( - - {type.name} - - - ) : null; - })} -
- )} + />
- )} +
+ + {/* Sorting */} +
+
+ + +
+
+ + +
+
-
- )} + )}
{/* Bulk Actions Toolbar */} @@ -577,7 +527,6 @@ export default function DocumentsPage() { documents={filteredDocuments.map((d) => ({ id: d.id, title: d.title, - status: d.status, }))} onClearSelection={selection.clearSelection} /> @@ -729,6 +678,26 @@ export default function DocumentsPage() { {doc.title} + {/* Tags - отображаем теги документа */} + {doc.tags && doc.tags.length > 0 && ( +
+ {doc.tags.map((tag) => ( + + + {tag.name} + + ))} +
+ )} + {/* Metadata - показываем только если есть хотя бы одно значение */} {(authorName || createdDate || doc.folder_path) && (
@@ -761,10 +730,6 @@ export default function DocumentsPage() { {/* Status Badges */}
- {doc.acknowledgement_required && ( doc.is_acknowledged ? ( @@ -802,13 +767,6 @@ export default function DocumentsPage() { Ознакомиться )} - - {/* Workflow Buttons */} -
@@ -818,8 +776,6 @@ export default function DocumentsPage() { )} - - )} )} @@ -842,300 +798,33 @@ export default function DocumentsPage() { /> - {/* Document Details Modal */} - setSelectedDocument(null)} - title={selectedDocument?.title} - size="lg" - > - {selectedDocument && ( -
- {/* Status Badge */} -
- -
- - {/* Metadata Grid */} -
- {/* Author */} -
-
- -
-
-

Автор

-

- {selectedDocument.uploaded_by - ? `${selectedDocument.uploaded_by.last_name} ${selectedDocument.uploaded_by.first_name}` - : selectedDocument.created_by - ? `${selectedDocument.created_by.last_name} ${selectedDocument.created_by.first_name}` - : "Не указан"} -

-
-
- - {/* Created Date */} -
-
- -
-
-

Дата создания

-

- {formatDate(selectedDocument.uploaded_at || selectedDocument.created_at)} -

-
-
- - {/* File Info */} - {selectedDocument.file_name && ( -
-
- -
-
-

Файл

-

- {selectedDocument.file_name} -

-
-
- )} - - {/* File Size */} - {selectedDocument.file_size && ( -
-
- -
-
-

Размер

-

- {(selectedDocument.file_size / 1024 / 1024).toFixed(2)} МБ -

-
-
- )} -
- - {/* Folder Path */} - {selectedDocument.folder_path && ( -
-
- - Расположение -
-

- {selectedDocument.folder_path} -

-
- )} - - {/* Description */} -
-

- - Описание -

-

- {selectedDocument.description || "Описание отсутствует"} -

-
- - {/* Recipients & Departments */} - {(selectedDocument.sent_to_all || - (selectedDocument.recipients && - selectedDocument.recipients.length > 0) || - (selectedDocument.departments && - selectedDocument.departments.length > 0)) && ( -
-

- - Получатели -

-
- {selectedDocument.sent_to_all && ( -
- - - Отправлено всем сотрудникам - -
- )} - - {selectedDocument.departments && - selectedDocument.departments.length > 0 && ( -
-
- - Отделы -
-
- {selectedDocument.departments.map((dept) => ( - - {dept.name} - - ))} -
-
- )} - - {selectedDocument.recipients && - selectedDocument.recipients.length > 0 && ( -
-
- - Конкретные получатели ( - {selectedDocument.recipients.length}) -
-
- {selectedDocument.recipients - .slice(0, 10) - .map((recipient) => ( -
- {recipient.last_name} {recipient.first_name} -
- ))} - {selectedDocument.recipients.length > 10 && ( -
- ... и ещё{" "} - {selectedDocument.recipients.length - 10} -
- )} -
-
- )} -
-
- )} - - {/* File Actions */} - {selectedDocument.file_url && (() => { - const fileExt = selectedDocument.file_name?.toLowerCase().split('.').pop() || ''; - const isPreviewable = [ - 'pdf', - 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp' - ].includes(fileExt); - - return ( -
- {isPreviewable && ( - - )} - - - Скачать - -
- ); - })()} - - {/* Workflow Actions */} -
-

- Действия -

- { - loadDocuments(); - setSelectedDocument(null); - }} - /> -
- - {/* Acknowledgement */} - {selectedDocument.acknowledgement_required && ( -
-
-

- Подтверждение прочтения -

- -
- { - loadDocuments(); - // Refresh selected document - apiClient - .getDocument(selectedDocument.id) - .then(setSelectedDocument); - }} - /> -
- )} - - {/* Comments */} -
- -
- - {/* Version History */} -
- { - loadDocuments(); - apiClient - .getDocument(selectedDocument.id) - .then(setSelectedDocument); - }} - /> -
- - {/* Related Documents */} -
- { - apiClient.getDocument(docId).then(setSelectedDocument); - }} - /> -
-
- )} -
+ onUpdate={() => { + loadDocuments(); + if (selectedDocument) { + apiClient.getDocument(selectedDocument.id).then(setSelectedDocument); + } + }} + onEditMetadata={() => { + setShowMetadataEditor(true); + }} + onViewReport={() => { + if (selectedDocument) { + setShowAcknowledgementsReport({ + documentId: selectedDocument.id, + documentTitle: selectedDocument.title, + }); + setSelectedDocument(null); + } + }} + onNavigateToRelated={(docId) => { + apiClient.getDocument(docId).then(setSelectedDocument); + }} + /> {/* File Preview Modal */} {previewFile && ( @@ -1237,6 +926,27 @@ export default function DocumentsPage() { onClose={() => setShowAcknowledgementsReport(null)} /> )} + + {/* Tag Management Modal */} + setShowTagManagement(false)} + onTagsUpdated={loadTags} + /> + + {/* Document Metadata Editor */} + {selectedDocument && ( + setShowMetadataEditor(false)} + document={selectedDocument} + onUpdate={() => { + loadDocuments(); + // Refresh selected document + apiClient.getDocument(selectedDocument.id).then(setSelectedDocument); + }} + /> + )}
); } diff --git a/frontend/src/components/documents/DocumentDetailModal.tsx b/frontend/src/components/documents/DocumentDetailModal.tsx index 0bbd0e7a..3e5ffca0 100644 --- a/frontend/src/components/documents/DocumentDetailModal.tsx +++ b/frontend/src/components/documents/DocumentDetailModal.tsx @@ -16,16 +16,18 @@ import { CheckCircle, Eye, Download, - Share2, - MoreVertical, ChevronRight, Activity, - History, + Maximize2, + Minimize2, + Info, + Edit, MessageSquare, + Link2, } from "lucide-react"; -import { DocumentStatusBadge } from "./DocumentStatusBadge"; -import { DocumentWorkflowButtons } from "./DocumentWorkflowButtons"; import { DocumentAcknowledgement } from "./DocumentAcknowledgement"; +import { DocumentComments } from "./DocumentComments"; +import { DocumentRelated } from "./DocumentRelated"; interface DocumentDetailModalProps { document: Document | null; @@ -33,9 +35,12 @@ interface DocumentDetailModalProps { onClose: () => void; onUpdate?: () => void; onPreview?: (url: string, name: string) => void; + onEditMetadata?: () => void; + onViewReport?: () => void; + onNavigateToRelated?: (docId: number) => void; } -type TabType = "overview" | "activity" | "acknowledgements" | "versions"; +type TabType = "preview" | "info" | "activity" | "acknowledgements" | "comments" | "related"; export function DocumentDetailModal({ document, @@ -43,8 +48,12 @@ export function DocumentDetailModal({ onClose, onUpdate, onPreview, + onEditMetadata, + onViewReport, + onNavigateToRelated, }: DocumentDetailModalProps) { - const [activeTab, setActiveTab] = useState("overview"); + const [activeTab, setActiveTab] = useState("preview"); + const [isFullscreen, setIsFullscreen] = useState(false); if (!document) return null; @@ -65,12 +74,64 @@ export function DocumentDetailModal({ }; const tabs = [ - { id: "overview" as TabType, label: "Обзор", icon: FileText }, + { id: "preview" as TabType, label: "Предпросмотр", icon: Eye }, + { id: "info" as TabType, label: "Информация", icon: Info }, { id: "activity" as TabType, label: "Активность", icon: Activity }, { id: "acknowledgements" as TabType, label: "Ознакомления", icon: CheckCircle }, - { id: "versions" as TabType, label: "Версии", icon: History }, + { id: "comments" as TabType, label: "Комментарии", icon: MessageSquare }, + { id: "related" as TabType, label: "Связанные", icon: Link2 }, ]; + const renderPreview = () => { + if (!document.file_url) { + return ( +
+
+ +

Файл не прикреплен

+
+
+ ); + } + + const fileExt = document.file_name?.toLowerCase().split(".").pop() || ""; + const isImage = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].includes(fileExt); + const isPDF = fileExt === "pdf"; + + if (isImage) { + return ( +
+ {document.file_name +
+ ); + } + + if (isPDF) { + return ( +