From 12f3b7b81b4f04cf58e507e1a47bf103b5a49390 Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Sat, 28 Feb 2026 08:30:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=D0=9F=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=81=20django-filer=20=D0=B8=20django-fsm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Установлены библиотеки: react-dropzone, react-pdf, pdfjs-dist, react-file-icon, @tanstack/react-table - Обновлены TypeScript типы для Document + FSM статусы - Обновлен API клиент: CRUD операции, FSM переходы, подтверждение прочтения - Создан DocumentUploadForm: drag&drop загрузка файлов - Создан DocumentStatusBadge: цветные статусы (draft/in_review/approved/published/archived/rejected) - Создан DocumentWorkflowButtons: динамические кнопки FSM переходов - Создан DocumentPreview: встроенный просмотр PDF с навигацией - Создан DocumentAcknowledgement: подтверждение прочтения с комментариями - Обновлена страница documents: фильтры, модальные окна, workflow UI - Добавлена документация в DOCUMENTS_IMPLEMENTATION.md --- frontend/DOCUMENTS_IMPLEMENTATION.md | 286 ++++++++++ frontend/package-lock.json | 506 ++++++++++++++++++ frontend/package.json | 6 + frontend/src/app/documents/page.tsx | 347 +++++++++--- .../documents/DocumentAcknowledgement.tsx | 130 +++++ .../components/documents/DocumentPreview.tsx | 147 +++++ .../documents/DocumentStatusBadge.tsx | 60 +++ .../documents/DocumentUploadForm.tsx | 238 ++++++++ .../documents/DocumentWorkflowButtons.tsx | 168 ++++++ frontend/src/components/documents/index.ts | 5 + frontend/src/lib/api.ts | 108 +++- frontend/src/types/api.ts | 18 + 12 files changed, 1944 insertions(+), 75 deletions(-) create mode 100644 frontend/DOCUMENTS_IMPLEMENTATION.md create mode 100644 frontend/src/components/documents/DocumentAcknowledgement.tsx create mode 100644 frontend/src/components/documents/DocumentPreview.tsx create mode 100644 frontend/src/components/documents/DocumentStatusBadge.tsx create mode 100644 frontend/src/components/documents/DocumentUploadForm.tsx create mode 100644 frontend/src/components/documents/DocumentWorkflowButtons.tsx create mode 100644 frontend/src/components/documents/index.ts diff --git a/frontend/DOCUMENTS_IMPLEMENTATION.md b/frontend/DOCUMENTS_IMPLEMENTATION.md new file mode 100644 index 00000000..a20a2dc8 --- /dev/null +++ b/frontend/DOCUMENTS_IMPLEMENTATION.md @@ -0,0 +1,286 @@ +# Документы - Frontend Implementation + +## Реализованный функционал + +### 1. **Загрузка документов** (django-filer) + +✅ Компонент `DocumentUploadForm`: +- Drag & Drop интерфейс для загрузки файлов +- Поддержка различных типов файлов (PDF, Word, Excel, изображения, текст) +- Валидация размера файлов +- Автоматическое заполнение названия из имени файла +- Форма с полями: название, тип, описание, теги +- Интеграция с API (`POST /api/v1/documents/`) + +### 2. **FSM Workflow** (django-fsm) + +✅ Компонент `DocumentStatusBadge`: +- Цветовые индикаторы для каждого статуса: + * 🟢 Черновик (серый) + * 🔵 На рассмотрении (голубой) + * 🟢 Утверждено (зеленый) + * 🔵 Опубликовано (синий) + * ⚪ В архиве (серый) + * 🔴 Отклонено (красный) + +✅ Компонент `DocumentWorkflowButtons`: +- Динамические кнопки действий в зависимости от текущего статуса +- Переходы: + * Черновик → Отправить на рассмотрение + * На рассмотрении → Утвердить / Отклонить / Вернуть в черновик + * Утверждено → Опубликовать + * Опубликовано → Архивировать + * В архиве → Разархивировать +- Подтверждающие диалоги для критичных действий +- Toast уведомления об успехе/ошибке + +### 3. **Preview файлов** + +✅ Компонент `DocumentPreview`: +- Встроенный просмотр PDF файлов с постраничной навигацией +- Использует `react-pdf` и `pdfjs-dist` +- Кнопки: следующая/предыдущая страница, скачать, закрыть +- Для не-PDF файлов: предложение скачать +- Полноэкранное модальное окно + +### 4. **Подтверждение прочтения** + +✅ Компонент `DocumentAcknowledgement`: +- Форма подтверждения с опциональным комментарием +- Индикатор "Прочитано" для текущего пользователя +- Список всех подтверждений с именами пользователей и датами +- Интеграция с API (`POST /api/v1/documents/{id}/acknowledge/`) + +### 5. **Список документов** + +✅ Обновленная страница `documents/page.tsx`: +- Поиск по названию, описанию, типу, автору +- Фильтрация по статусу (все/черновик/на рассмотрении/...) +- Карточки документов с: + * Статус badge + * Workflow кнопки + * Теги + * Метаданные (автор, даты) + * Кнопки "Просмотр" и "Детали" +- Модальное окно с детальной информацией о документе +- Модальное окно загрузки нового документа + +## API интеграция + +### Обновленный `apiClient` (`lib/api.ts`) + +Добавлены методы: + +```typescript +// CRUD операции +getDocuments(params?) // GET /api/v1/documents/ +getDocument(id) // GET /api/v1/documents/{id}/ +createDocument(data) // POST /api/v1/documents/ +updateDocument(id, data) // PATCH /api/v1/documents/{id}/ +deleteDocument(id) // DELETE /api/v1/documents/{id}/ + +// FSM переходы +submitDocumentForReview(id) // POST /api/v1/documents/{id}/submit-for-review/ +approveDocument(id) // POST /api/v1/documents/{id}/approve/ +rejectDocument(id) // POST /api/v1/documents/{id}/reject/ +publishDocument(id) // POST /api/v1/documents/{id}/publish/ +returnDocumentToDraft(id) // POST /api/v1/documents/{id}/return-to-draft/ +archiveDocument(id) // POST /api/v1/documents/{id}/archive/ +unarchiveDocument(id) // POST /api/v1/documents/{id}/unarchive/ + +// Подтверждение прочтения +acknowledgeDocument(id, notes?) // POST /api/v1/documents/{id}/acknowledge/ +``` + +## TypeScript типы + +### Обновленный `types/api.ts` + +```typescript +export type DocumentStatus = + | 'draft' + | 'in_review' + | 'approved' + | 'published' + | 'archived' + | 'rejected'; + +export interface DocumentAcknowledgement { + id: number; + document: number; + user: User; + acknowledged_at: string; + notes?: string; +} + +export interface Document { + id: number; + title: string; + description?: string; + file?: string; + file_url?: string; + file_name?: string; + file_size?: number; + document_type: string; + status: string; // human-readable + status_code: DocumentStatus; // machine-readable + created_by: User; + created_at: string; + updated_at: string; + tags?: string[]; + acknowledgements?: DocumentAcknowledgement[]; + acknowledgement_required?: boolean; + is_acknowledged?: boolean; +} +``` + +## Установленные библиотеки + +```json +{ + "dependencies": { + "react-dropzone": "^14.2.3", + "react-pdf": "^9.1.1", + "pdfjs-dist": "^4.8.69", + "react-file-icon": "^1.5.0", + "@tanstack/react-table": "^8.20.5" + } +} +``` + +## Структура файлов + +``` +frontend/src/ +├── components/ +│ └── documents/ +│ ├── index.ts +│ ├── DocumentUploadForm.tsx +│ ├── DocumentStatusBadge.tsx +│ ├── DocumentWorkflowButtons.tsx +│ ├── DocumentPreview.tsx +│ └── DocumentAcknowledgement.tsx +├── app/ +│ └── documents/ +│ └── page.tsx (обновлена) +├── lib/ +│ └── api.ts (обновлен) +└── types/ + └── api.ts (обновлен) +``` + +## Использование компонентов + +### DocumentUploadForm + +```tsx +import { DocumentUploadForm } from "@/components/documents"; + + { + // Обновить список документов + }} + onCancel={() => { + // Закрыть форму + }} +/> +``` + +### DocumentStatusBadge + +```tsx +import { DocumentStatusBadge } from "@/components/documents"; + + +``` + +### DocumentWorkflowButtons + +```tsx +import { DocumentWorkflowButtons } from "@/components/documents"; + + { + // Обновить данные + }} +/> +``` + +### DocumentPreview + +```tsx +import { DocumentPreview } from "@/components/documents"; + + setShowPreview(false)} +/> +``` + +### DocumentAcknowledgement + +```tsx +import { DocumentAcknowledgement } from "@/components/documents"; + + { + // Обновить данные + }} +/> +``` + +## Roadmap / Будущие улучшения + +### Приоритет ВЫСОКИЙ +- [ ] Добавить права доступа на UI уровне (скрывать кнопки для пользователей без прав) +- [ ] Версионирование документов (интеграция с django-reversion) +- [ ] История изменений статуса (timeline view) + +### Приоритет СРЕДНИЙ +- [ ] Редактирование документов (обновление файла, метаданных) +- [ ] Массовые операции (архивирование нескольких документов) +- [ ] Экспорт списка документов (CSV, Excel) +- [ ] Уведомления о смене статуса + +### Приоритет НИЗКИЙ +- [ ] OCR для сканов (pytesseract на бэкенде) +- [ ] Полнотекстовый поиск по содержимому PDF +- [ ] Интеграция с внешними хранилищами (Google Drive, OneDrive) +- [ ] Шаблоны документов + +## Тестирование + +### Checklist для проверки + +- [ ] Загрузка документа через Drag & Drop +- [ ] Загрузка документа через выбор файла +- [ ] Поиск по документам +- [ ] Фильтрация по статусу +- [ ] Переход: Черновик → На рассмотрении +- [ ] Переход: На рассмотрении → Утверждено +- [ ] Переход: Утверждено → Опубликовано +- [ ] Preview PDF файла +- [ ] Подтверждение прочтения документа +- [ ] Просмотр списка подтверждений +- [ ] Модальное окно с деталями документа + +## Известные проблемы + +1. **PDF.js Worker Warning**: + - В консоли может появляться предупреждение о worker + - Решение: уже настроен через unpkg CDN в `DocumentPreview.tsx` + +2. **API Permission Tests**: + - ~10 тестов в бэкенде падают (проблема с правами доступа) + - Не блокирует работу frontend + +## Контакты + +Для вопросов и предложений обращайтесь к команде разработки. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f475e40..c6d998e0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,12 +10,17 @@ "dependencies": { "@chatscope/chat-ui-kit-react": "^2.1.1", "@chatscope/chat-ui-kit-styles": "^1.4.0", + "@tanstack/react-table": "^8.21.3", "date-fns": "^4.1.0", "lucide-react": "^0.574.0", "next": "16.1.6", + "pdfjs-dist": "^5.4.624", "react": "19.2.3", "react-big-calendar": "^1.19.4", "react-dom": "19.2.3", + "react-dropzone": "^15.0.0", + "react-file-icon": "^1.6.0", + "react-pdf": "^10.4.1", "sonner": "^2.0.7" }, "devDependencies": { @@ -24,6 +29,7 @@ "@types/react": "^19", "@types/react-big-calendar": "^1.16.3", "@types/react-dom": "^19", + "@types/react-pdf": "^6.2.0", "cross-env": "^10.1.0", "eslint": "^9", "eslint-config-next": "16.1.6", @@ -1065,6 +1071,256 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.95.tgz", + "integrity": "sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.95", + "@napi-rs/canvas-darwin-arm64": "0.1.95", + "@napi-rs/canvas-darwin-x64": "0.1.95", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.95", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.95", + "@napi-rs/canvas-linux-arm64-musl": "0.1.95", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.95", + "@napi-rs/canvas-linux-x64-gnu": "0.1.95", + "@napi-rs/canvas-linux-x64-musl": "0.1.95", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.95", + "@napi-rs/canvas-win32-x64-msvc": "0.1.95" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.95.tgz", + "integrity": "sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.95.tgz", + "integrity": "sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.95.tgz", + "integrity": "sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.95.tgz", + "integrity": "sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.95.tgz", + "integrity": "sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.95.tgz", + "integrity": "sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.95.tgz", + "integrity": "sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.95.tgz", + "integrity": "sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.95.tgz", + "integrity": "sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.95.tgz", + "integrity": "sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.95", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.95.tgz", + "integrity": "sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1547,6 +1803,39 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1628,6 +1917,36 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-pdf": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-pdf/-/react-pdf-6.2.0.tgz", + "integrity": "sha512-OSCYmrfaJvpXkM5V4seUMAhUDOAOqbGQf9kwv14INyTf7AjDs2ukfkkQrLWRQ8OjWrDklbXYWh5l7pT7l0N76g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "pdfjs-dist": "^2.16.105" + } + }, + "node_modules/@types/react-pdf/node_modules/pdfjs-dist": { + "version": "2.16.105", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", + "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dommatrix": "^1.0.3", + "web-streams-polyfill": "^3.2.1" + }, + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -2387,6 +2706,15 @@ "node": ">= 0.4" } }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2619,6 +2947,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2844,6 +3178,14 @@ "csstype": "^3.0.2" } }, + "node_modules/dommatrix": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", + "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", + "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix.", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3551,6 +3893,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4826,6 +5180,24 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-cancellable-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz", + "integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-event-props": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz", + "integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4841,6 +5213,23 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, + "node_modules/merge-refs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz", + "integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5046,6 +5435,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-readable-to-web-readable-stream": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz", + "integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==", + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5266,6 +5662,19 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pdfjs-dist": { + "version": "5.4.624", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.624.tgz", + "integrity": "sha512-sm6TxKTtWv1Oh6n3C6J6a8odejb5uO4A4zo/2dgkHuC0iu8ZMAXOezEODkVaoVp8nX1Xzr+0WxFJJmUr45hQzg==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.88", + "node-readable-to-web-readable-stream": "^0.4.2" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5417,6 +5826,37 @@ "react": "^19.2.3" } }, + "node_modules/react-dropzone": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", + "integrity": "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-file-icon": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-file-icon/-/react-file-icon-1.6.0.tgz", + "integrity": "sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0", + "react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5448,6 +5888,56 @@ "react-dom": ">=16.3.0" } }, + "node_modules/react-pdf": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.1.tgz", + "integrity": "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "dequal": "^2.0.3", + "make-cancellable-promise": "^2.0.0", + "make-event-props": "^2.0.0", + "merge-refs": "^2.0.0", + "pdfjs-dist": "5.4.296", + "tiny-invariant": "^1.0.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-pdf/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-pdf/node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6052,6 +6542,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6395,6 +6891,16 @@ "loose-envify": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index b7b27ab5..4d0d6914 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,12 +12,17 @@ "dependencies": { "@chatscope/chat-ui-kit-react": "^2.1.1", "@chatscope/chat-ui-kit-styles": "^1.4.0", + "@tanstack/react-table": "^8.21.3", "date-fns": "^4.1.0", "lucide-react": "^0.574.0", "next": "16.1.6", + "pdfjs-dist": "^5.4.624", "react": "19.2.3", "react-big-calendar": "^1.19.4", "react-dom": "19.2.3", + "react-dropzone": "^15.0.0", + "react-file-icon": "^1.6.0", + "react-pdf": "^10.4.1", "sonner": "^2.0.7" }, "devDependencies": { @@ -26,6 +31,7 @@ "@types/react": "^19", "@types/react-big-calendar": "^1.16.3", "@types/react-dom": "^19", + "@types/react-pdf": "^6.2.0", "cross-env": "^10.1.0", "eslint": "^9", "eslint-config-next": "16.1.6", diff --git a/frontend/src/app/documents/page.tsx b/frontend/src/app/documents/page.tsx index 4128543b..32af646c 100644 --- a/frontend/src/app/documents/page.tsx +++ b/frontend/src/app/documents/page.tsx @@ -3,8 +3,13 @@ import { AppShell } from "../../components/AppShell"; import { apiClient } from "@/lib/api"; import { useEffect, useMemo, useState } from "react"; -import type { Document } from "@/types/api"; -import { Search, FileText, Download } from "lucide-react"; +import type { Document, DocumentStatus } from "@/types/api"; +import { Search, FileText, Plus, Eye, X } from "lucide-react"; +import { DocumentUploadForm } from "@/components/documents/DocumentUploadForm"; +import { DocumentStatusBadge } from "@/components/documents/DocumentStatusBadge"; +import { DocumentWorkflowButtons } from "@/components/documents/DocumentWorkflowButtons"; +import { DocumentPreview } from "@/components/documents/DocumentPreview"; +import { DocumentAcknowledgement } from "@/components/documents/DocumentAcknowledgement"; function formatDate(value?: string): string { if (!value) return ""; @@ -20,26 +25,34 @@ function formatDate(value?: string): string { export default function DocumentsPage() { const [documents, setDocuments] = useState([]); const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [showUploadForm, setShowUploadForm] = useState(false); + const [selectedDocument, setSelectedDocument] = useState(null); + const [previewFile, setPreviewFile] = useState<{ url: string; name: string } | null>(null); + + const loadDocuments = async () => { + try { + setLoading(true); + setError(null); + const params: any = {}; + if (statusFilter !== "all") { + params.status = statusFilter; + } + const response = await apiClient.getDocuments(params); + setDocuments(response.results || []); + } catch (err) { + console.error("Ошибка загрузки документов:", err); + setError("Не удалось загрузить документы"); + } finally { + setLoading(false); + } + }; useEffect(() => { - async function loadDocuments() { - try { - setLoading(true); - setError(null); - const response = await apiClient.getDocuments(); - setDocuments(response.results || []); - } catch (err) { - console.error("Ошибка загрузки документов:", err); - setError("Не удалось загрузить документы"); - } finally { - setLoading(false); - } - } - loadDocuments(); - }, []); + }, [statusFilter]); const filteredDocuments = useMemo(() => { const q = search.trim().toLowerCase(); @@ -74,73 +87,259 @@ export default function DocumentsPage() {

{error}

) : ( -
-
- - setSearch(e.target.value)} - placeholder="Поиск по документам" - className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2.5 pl-9 pr-3 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" - /> -
+ <> +
+ {/* Header */} +
+

Документы

+ +
-
- {filteredDocuments.length === 0 ? ( -
- -

Документы не найдены

+ {/* Filters */} +
+
+ + setSearch(e.target.value)} + placeholder="Поиск по документам" + className="w-full rounded-lg border border-gray-200 bg-gray-50 py-2.5 pl-9 pr-3 text-sm text-gray-800 transition focus:border-sky-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-sky-100" + />
- ) : ( - filteredDocuments.map((doc) => { - const authorName = doc.created_by - ? `${doc.created_by.last_name} ${doc.created_by.first_name}`.trim() - : "Неизвестно"; - return ( -
-
-
-

{doc.title}

-

Тип: {doc.document_type || "—"}

+ +
+ + {/* Documents List */} +
+ {filteredDocuments.length === 0 ? ( +
+ +

Документы не найдены

+
+ ) : ( + filteredDocuments.map((doc) => { + const authorName = doc.created_by + ? `${doc.created_by.last_name} ${doc.created_by.first_name}`.trim() + : "Неизвестно"; + + return ( +
+
+
+
+

{doc.title}

+ +
+

Тип: {doc.document_type || "—"}

+
+ +
+ {doc.file_url && ( + + )} + +
- {doc.file ? ( - - - Открыть - - ) : null} -
+

+ {doc.description || "Описание не заполнено"} +

-

{doc.description || "Описание не заполнено"}

+ {doc.tags && doc.tags.length > 0 && ( +
+ {doc.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} - {doc.tags && doc.tags.length > 0 ? ( -
- {doc.tags.map((tag) => ( - - #{tag} - - ))} + {/* Workflow Buttons */} +
+
- ) : null} -
-

Автор: {authorName}

-

Создано: {formatDate(doc.created_at)}

-

Обновлено: {formatDate(doc.updated_at)}

+
+

Автор: {authorName}

+

Создано: {formatDate(doc.created_at)}

+

Обновлено: {formatDate(doc.updated_at)}

+
+
+ ); + }) + )} +
+
+ + {/* Upload Modal */} + {showUploadForm && ( +
+
+
+

Загрузить документ

+ +
+ { + setShowUploadForm(false); + loadDocuments(); + }} + onCancel={() => setShowUploadForm(false)} + /> +
+
+ )} + + {/* Document Details Modal */} + {selectedDocument && ( +
+
+
+

+ {selectedDocument.title} +

+ +
+ +
+
+ +
+ +
+

Описание

+

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

+
+ +
+

Тип

+

{selectedDocument.document_type}

+
+ + {selectedDocument.file_url && ( +
+
- - ); - }) - )} -
-
+ )} + + {/* Workflow Actions */} +
+

Действия

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

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

+ { + loadDocuments(); + // Refresh selected document + apiClient.getDocument(selectedDocument.id).then(setSelectedDocument); + }} + /> +
+ )} + + + + )} + + {/* File Preview Modal */} + {previewFile && ( + setPreviewFile(null)} + /> + )} + )} ); diff --git a/frontend/src/components/documents/DocumentAcknowledgement.tsx b/frontend/src/components/documents/DocumentAcknowledgement.tsx new file mode 100644 index 00000000..b58ffce5 --- /dev/null +++ b/frontend/src/components/documents/DocumentAcknowledgement.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useState } from "react"; +import { CheckCircle, Users } from "lucide-react"; +import { apiClient } from "@/lib/api"; +import { toast } from "sonner"; +import type { Document } from "@/types/api"; + +interface DocumentAcknowledgementProps { + document: Document; + onAcknowledge?: () => void; +} + +export function DocumentAcknowledgement({ + document, + onAcknowledge, +}: DocumentAcknowledgementProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [showAcknowledgements, setShowAcknowledgements] = useState(false); + const [notes, setNotes] = useState(""); + + const handleAcknowledge = async () => { + setIsSubmitting(true); + try { + await apiClient.acknowledgeDocument(document.id, notes || undefined); + toast.success("Прочтение подтверждено"); + setNotes(""); + if (onAcknowledge) { + onAcknowledge(); + } + } catch (err) { + console.error("Ошибка подтверждения:", err); + toast.error("Не удалось подтвердить прочтение"); + } finally { + setIsSubmitting(false); + } + }; + + const acknowledgements = document.acknowledgements || []; + const isAcknowledged = document.is_acknowledged || false; + + return ( +
+ {/* Acknowledge Button/Status */} + {!isAcknowledged ? ( +
+
+ +
+

+ Требуется подтверждение прочтения +

+

+ Пожалуйста, подтвердите, что вы прочитали этот документ +

+
+
+ +