diff --git a/backend/api/v1/documents/serializers.py b/backend/api/v1/documents/serializers.py index cf21dfd2..5bd2ece9 100644 --- a/backend/api/v1/documents/serializers.py +++ b/backend/api/v1/documents/serializers.py @@ -109,6 +109,72 @@ class DepartmentBriefSerializer(serializers.Serializer): name = serializers.CharField(read_only=True) +class FolderSerializer(serializers.Serializer): + """Сериализатор для папки filer.Folder.""" + id = serializers.IntegerField(read_only=True) + name = serializers.CharField() + parent_id = serializers.IntegerField(source='parent.id', read_only=True, allow_null=True) + path = serializers.SerializerMethodField() + + def get_path(self, obj) -> str: + """Возвращает полный путь папки.""" + path_parts = [] + current = obj + while current: + path_parts.insert(0, current.name) + current = current.parent + return ' / '.join(path_parts) + + +class FolderBriefSerializer(serializers.Serializer): + """Краткий сериализатор папки для вложенного отображения.""" + id = serializers.IntegerField(read_only=True) + name = serializers.CharField(read_only=True) + + +class DocumentTypeSerializer(serializers.Serializer): + """Сериализатор для типов документов.""" + id = serializers.IntegerField(read_only=True) + name = serializers.CharField() + code = serializers.SlugField() + description = serializers.CharField(required=False, allow_blank=True) + icon = serializers.CharField(required=False) + color = serializers.CharField(required=False) + is_active = serializers.BooleanField(default=True) + + +class DocumentTagSerializer(serializers.Serializer): + """Сериализатор для тегов документов.""" + id = serializers.IntegerField(read_only=True) + name = serializers.CharField() + slug = serializers.SlugField(required=False) + color = serializers.CharField(required=False) + description = serializers.CharField(required=False, allow_blank=True) + document_count = serializers.SerializerMethodField() + + def get_document_count(self, obj) -> int: + """Возвращает количество документов с этим тегом.""" + return obj.documents.count() if hasattr(obj, 'documents') else 0 + + +class CabinetSerializer(serializers.Serializer): + """Сериализатор для кабинетов (виртуальных коллекций).""" + id = serializers.IntegerField(read_only=True) + name = serializers.CharField() + slug = serializers.SlugField(required=False) + description = serializers.CharField(required=False, allow_blank=True) + parent_id = serializers.IntegerField(source='parent.id', required=False, allow_null=True) + icon = serializers.CharField(required=False) + color = serializers.CharField(required=False) + document_count = serializers.SerializerMethodField() + created_by = EmployeeBriefSerializer(read_only=True) + created_at = serializers.DateTimeField(read_only=True) + + def get_document_count(self, obj) -> int: + """Возвращает количество документов в кабинете.""" + return obj.documents.count() if hasattr(obj, 'documents') else 0 + + class DocumentReadSerializer(serializers.ModelSerializer): """Сериализатор для чтения документа. @@ -116,9 +182,16 @@ class DocumentReadSerializer(serializers.ModelSerializer): """ uploaded_by = EmployeeBriefSerializer(read_only=True) + modified_by = EmployeeBriefSerializer(read_only=True) recipients = EmployeeBriefSerializer(many=True, read_only=True) departments = DepartmentBriefSerializer(many=True, read_only=True) + folder = FolderBriefSerializer(read_only=True) + folder_path = serializers.CharField(read_only=True) + document_type = DocumentTypeSerializer(read_only=True) + tags = DocumentTagSerializer(many=True, read_only=True) file_url = FilerFileSerializerField(source="file", read_only=True) + file_name = serializers.CharField(source='file.name', read_only=True, allow_null=True) + file_size = serializers.IntegerField(source='file.size', read_only=True, allow_null=True) is_acknowledged = serializers.SerializerMethodField() status = serializers.CharField(source='get_status_display', read_only=True) status_code = serializers.CharField(source='status', read_only=True) @@ -129,14 +202,23 @@ class DocumentReadSerializer(serializers.ModelSerializer): "id", "title", "description", + "extracted_text", # Для полнотекстового поиска + "folder", # Папка документа + "folder_path", # Полный путь папки + "document_type", # Тип документа + "tags", # Теги "status", # Человекочитаемый статус "status_code", # Код статуса для программной обработки "uploaded_by", "uploaded_at", + "modified_by", + "modified_at", "sent_to_all", "departments", "recipients", "file_url", + "file_name", + "file_size", "is_acknowledged", ) diff --git a/backend/documents/migrations/0007_add_folder_support.py b/backend/documents/migrations/0007_add_folder_support.py new file mode 100644 index 00000000..7cf2198c --- /dev/null +++ b/backend/documents/migrations/0007_add_folder_support.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.4 on 2026-02-28 05:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0006_add_fsm_status'), + ('filer', '0018_alter_file_options'), + ] + + operations = [ + migrations.AddField( + model_name='document', + name='folder', + field=models.ForeignKey(blank=True, help_text='Папка для организации документов в иерархической структуре', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='filer.folder', verbose_name='Папка'), + ), + ] diff --git a/backend/documents/migrations/0008_add_extended_models.py b/backend/documents/migrations/0008_add_extended_models.py new file mode 100644 index 00000000..36920997 --- /dev/null +++ b/backend/documents/migrations/0008_add_extended_models.py @@ -0,0 +1,128 @@ +# Generated by Django 5.2.4 on 2026-02-28 06:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0007_add_folder_support'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='DocumentType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Название')), + ('code', models.SlugField(unique=True, verbose_name='Код')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('metadata_schema', models.JSONField(blank=True, help_text='JSON Schema для валидации метаданных документов этого типа', null=True, verbose_name='Схема метаданных')), + ('icon', models.CharField(default='file-earmark-text', help_text='Bootstrap Icons класс', max_length=50, verbose_name='Иконка')), + ('color', models.CharField(default='#0d6efd', help_text='HEX цвет для UI', max_length=7, verbose_name='Цвет')), + ('is_active', models.BooleanField(default=True, verbose_name='Активен')), + ('order', models.IntegerField(default=0, verbose_name='Порядок')), + ], + options={ + 'verbose_name': 'Тип документа', + 'verbose_name_plural': 'Типы документов', + 'ordering': ['order', 'name'], + }, + ), + migrations.AddField( + model_name='document', + name='extracted_text', + field=models.TextField(blank=True, editable=False, help_text='Текст извлечен из файла для полнотекстового поиска', verbose_name='Извлеченный текст'), + ), + migrations.AddField( + model_name='document', + name='modified_at', + field=models.DateTimeField(auto_now=True, verbose_name='Дата изменения'), + ), + migrations.AddField( + model_name='document', + name='modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_documents', to=settings.AUTH_USER_MODEL, verbose_name='Последнее изменение'), + ), + migrations.CreateModel( + name='Cabinet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Название')), + ('slug', models.SlugField(unique=True, verbose_name='Слаг')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('icon', models.CharField(default='folder', help_text='Bootstrap Icons класс', max_length=50, verbose_name='Иконка')), + ('color', models.CharField(default='#6c757d', help_text='HEX цвет для UI', max_length=7, verbose_name='Цвет')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')), + ('order', models.IntegerField(default=0, verbose_name='Порядок')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_cabinets', to=settings.AUTH_USER_MODEL, verbose_name='Создал')), + ('documents', models.ManyToManyField(blank=True, related_name='cabinets', to='documents.document', verbose_name='Документы')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='documents.cabinet', verbose_name='Родительский кабинет')), + ], + options={ + 'verbose_name': 'Кабинет', + 'verbose_name_plural': 'Кабинеты', + 'ordering': ['order', 'name'], + }, + ), + migrations.CreateModel( + name='DocumentTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Название')), + ('slug', models.SlugField(unique=True, verbose_name='Слаг')), + ('color', models.CharField(default='#0d6efd', help_text='HEX цвет для UI', max_length=7, verbose_name='Цвет')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')), + ('documents', models.ManyToManyField(blank=True, related_name='tags', to='documents.document', verbose_name='Документы')), + ], + options={ + 'verbose_name': 'Тег', + 'verbose_name_plural': 'Теги', + 'ordering': ['name'], + }, + ), + migrations.AddField( + model_name='document', + name='document_type', + field=models.ForeignKey(blank=True, help_text='Тип документа определяет шаблон метаданных и workflow', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.documenttype', verbose_name='Тип документа'), + ), + migrations.CreateModel( + name='DocumentAuditLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('action', models.CharField(choices=[('created', 'Создан'), ('viewed', 'Просмотрен'), ('downloaded', 'Скачан'), ('edited', 'Изменен'), ('moved', 'Перемещен'), ('deleted', 'Удален'), ('status_changed', 'Изменен статус'), ('permissions_changed', 'Изменены права'), ('signed', 'Подписан')], max_length=50, verbose_name='Действие')), + ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Время')), + ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP адрес')), + ('user_agent', models.TextField(blank=True, verbose_name='User Agent')), + ('metadata', models.JSONField(blank=True, help_text='Дополнительная информация о действии', null=True, verbose_name='Метаданные')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='audit_log', to='documents.document', verbose_name='Документ')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Запись аудита', + 'verbose_name_plural': 'Записи аудита', + 'ordering': ['-timestamp'], + 'indexes': [models.Index(fields=['document', '-timestamp'], name='documents_d_documen_5c0383_idx'), models.Index(fields=['user', '-timestamp'], name='documents_d_user_id_146cf5_idx'), models.Index(fields=['action', '-timestamp'], name='documents_d_action_c6b54b_idx')], + }, + ), + migrations.CreateModel( + name='DocumentMetadata', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=100, verbose_name='Ключ')), + ('value', models.TextField(verbose_name='Значение')), + ('value_type', models.CharField(choices=[('text', 'Текст'), ('number', 'Число'), ('date', 'Дата'), ('boolean', 'Да/Нет'), ('url', 'Ссылка')], default='text', max_length=20, verbose_name='Тип значения')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='metadata_entries', to='documents.document', verbose_name='Документ')), + ], + options={ + 'verbose_name': 'Метаданные документа', + 'verbose_name_plural': 'Метаданные документов', + 'ordering': ['key'], + 'unique_together': {('document', 'key')}, + }, + ), + ] diff --git a/backend/documents/models.py b/backend/documents/models.py index 30d56cf5..436e30a3 100644 --- a/backend/documents/models.py +++ b/backend/documents/models.py @@ -69,8 +69,39 @@ class Document(models.Model): help_text=_('Файл, загруженный через django-filer') ) + # Папка для организации документов (иерархическая структура) + folder = models.ForeignKey( + 'filer.Folder', + verbose_name=_('Папка'), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='documents', + help_text=_('Папка для организации документов в иерархической структуре') + ) + description = models.TextField(_('Описание'), blank=True) - + + # Тип документа (Договор, Приказ, Акт и т.д.) + document_type = models.ForeignKey( + 'DocumentType', + verbose_name=_('Тип документа'), + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='documents', + help_text=_('Тип документа определяет шаблон метаданных и workflow') + ) + + # Full-text search - извлеченный текст (OCR или парсинг PDF/DOCX) + extracted_text = models.TextField( + _('Извлеченный текст'), + blank=True, + editable=False, + help_text=_('Текст извлечен из файла для полнотекстового поиска') + ) + + # Audit trail uploaded_by = models.ForeignKey( User, verbose_name=_('Загрузил'), @@ -80,6 +111,16 @@ class Document(models.Model): related_name='uploaded_documents' ) uploaded_at = models.DateTimeField(_('Дата загрузки'), auto_now_add=True) + + modified_by = models.ForeignKey( + User, + verbose_name=_('Последнее изменение'), + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='modified_documents' + ) + modified_at = models.DateTimeField(_('Дата изменения'), auto_now=True) # Логика распространения документов sent_to_all = models.BooleanField( @@ -213,6 +254,44 @@ class Document(models.Model): except Exception: # Если thumbnail не удалось создать (не изображение), возвращаем иконку по типу return self.file.url + + @property + def folder_path(self): + """ + Полный путь папки документа (breadcrumbs). + + Returns: + str: Путь вида "Корень / Папка1 / Папка2" или пустая строка + """ + if not self.folder: + return '' + + # Собираем путь от корня до текущей папки + path_parts = [] + current = self.folder + while current: + path_parts.insert(0, current.name) + current = current.parent + + return ' / '.join(path_parts) + + def get_folder_hierarchy(self): + """ + Получить иерархию папок от корня до текущей. + + Returns: + list: Список словарей [{id, name}, ...] от корня к текущей папке + """ + if not self.folder: + return [] + + hierarchy = [] + current = self.folder + while current: + hierarchy.insert(0, {'id': current.id, 'name': current.name}) + current = current.parent + + return hierarchy @reversion.register() @@ -244,3 +323,253 @@ class DocumentAcknowledgement(models.Model): return f'{self.user} — {self.document} @ {self.acknowledged_at:%d.%m.%Y %H:%M}' +# ============================================================================ +# ТИПЫ ДОКУМЕНТОВ И МЕТАДАННЫЕ +# ============================================================================ + +class DocumentType(models.Model): + """ + Типы документов (Договор, Приказ, Акт, Счет и т.д.). + Определяет структуру метаданных и workflow для документов этого типа. + """ + name = models.CharField(_('Название'), max_length=100) + code = models.SlugField(_('Код'), unique=True, max_length=50) + description = models.TextField(_('Описание'), blank=True) + + # JSON Schema для валидации метаданных этого типа + metadata_schema = models.JSONField( + _('Схема метаданных'), + null=True, + blank=True, + help_text=_('JSON Schema для валидации метаданных документов этого типа') + ) + + # Иконка для UI + icon = models.CharField( + _('Иконка'), + max_length=50, + default='file-earmark-text', + help_text=_('Bootstrap Icons класс') + ) + + # Цвет для UI + color = models.CharField( + _('Цвет'), + max_length=7, + default='#0d6efd', + help_text=_('HEX цвет для UI') + ) + + is_active = models.BooleanField(_('Активен'), default=True) + order = models.IntegerField(_('Порядок'), default=0) + + class Meta: + verbose_name = _('Тип документа') + verbose_name_plural = _('Типы документов') + ordering = ['order', 'name'] + + def __str__(self): + return self.name + + +class DocumentMetadata(models.Model): + """ + Динамические метаданные документа (key-value пары). + Позволяет добавлять произвольные поля к документам. + """ + document = models.ForeignKey( + Document, + on_delete=models.CASCADE, + related_name='metadata_entries', + verbose_name=_('Документ') + ) + key = models.CharField(_('Ключ'), max_length=100) + value = models.TextField(_('Значение')) + + # Тип значения для правильного отображения и валидации + class ValueType(models.TextChoices): + TEXT = 'text', _('Текст') + NUMBER = 'number', _('Число') + DATE = 'date', _('Дата') + BOOLEAN = 'boolean', _('Да/Нет') + URL = 'url', _('Ссылка') + + value_type = models.CharField( + _('Тип значения'), + max_length=20, + choices=ValueType.choices, + default=ValueType.TEXT + ) + + class Meta: + verbose_name = _('Метаданные документа') + verbose_name_plural = _('Метаданные документов') + unique_together = ('document', 'key') + ordering = ['key'] + + def __str__(self): + return f'{self.key}: {self.value}' + + +class DocumentTag(models.Model): + """ + Теги для категоризации документов. + Аналог Mayan EDMS tags. + """ + name = models.CharField(_('Название'), max_length=100, unique=True) + slug = models.SlugField(_('Слаг'), unique=True) + color = models.CharField( + _('Цвет'), + max_length=7, + default='#0d6efd', + help_text=_('HEX цвет для UI') + ) + description = models.TextField(_('Описание'), blank=True) + + documents = models.ManyToManyField( + Document, + related_name='tags', + blank=True, + verbose_name=_('Документы') + ) + + created_at = models.DateTimeField(_('Создан'), auto_now_add=True) + + class Meta: + verbose_name = _('Тег') + verbose_name_plural = _('Теги') + ordering = ['name'] + + def __str__(self): + return self.name + + +class Cabinet(models.Model): + """ + Кабинеты - виртуальные хранилища документов. + Аналог Mayan EDMS cabinets. Позволяет организовать документы + в виртуальные коллекции независимо от физической структуры папок. + """ + name = models.CharField(_('Название'), max_length=200) + slug = models.SlugField(_('Слаг'), unique=True) + description = models.TextField(_('Описание'), blank=True) + + # Иерархическая структура кабинетов + parent = models.ForeignKey( + 'self', + verbose_name=_('Родительский кабинет'), + null=True, + blank=True, + on_delete=models.CASCADE, + related_name='children' + ) + + documents = models.ManyToManyField( + Document, + related_name='cabinets', + blank=True, + verbose_name=_('Документы') + ) + + # Иконка и цвет для UI + icon = models.CharField( + _('Иконка'), + max_length=50, + default='folder', + help_text=_('Bootstrap Icons класс') + ) + color = models.CharField( + _('Цвет'), + max_length=7, + default='#6c757d', + help_text=_('HEX цвет для UI') + ) + + created_by = models.ForeignKey( + User, + verbose_name=_('Создал'), + on_delete=models.SET_NULL, + null=True, + related_name='created_cabinets' + ) + created_at = models.DateTimeField(_('Создан'), auto_now_add=True) + + order = models.IntegerField(_('Порядок'), default=0) + + class Meta: + verbose_name = _('Кабинет') + verbose_name_plural = _('Кабинеты') + ordering = ['order', 'name'] + + def __str__(self): + if self.parent: + return f'{self.parent.name} / {self.name}' + return self.name + + +# ============================================================================ +# АУДИТ И ИСТОРИЯ +# ============================================================================ + +class DocumentAuditLog(models.Model): + """ + Полный аудит всех действий с документами. + Фиксирует кто, когда и что сделал с документом. + """ + document = models.ForeignKey( + Document, + on_delete=models.CASCADE, + related_name='audit_log', + verbose_name=_('Документ') + ) + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + verbose_name=_('Пользователь') + ) + + class Action(models.TextChoices): + CREATED = 'created', _('Создан') + VIEWED = 'viewed', _('Просмотрен') + DOWNLOADED = 'downloaded', _('Скачан') + EDITED = 'edited', _('Изменен') + MOVED = 'moved', _('Перемещен') + DELETED = 'deleted', _('Удален') + STATUS_CHANGED = 'status_changed', _('Изменен статус') + PERMISSIONS_CHANGED = 'permissions_changed', _('Изменены права') + SIGNED = 'signed', _('Подписан') + + action = models.CharField( + _('Действие'), + max_length=50, + choices=Action.choices + ) + timestamp = models.DateTimeField(_('Время'), auto_now_add=True) + ip_address = models.GenericIPAddressField( + _('IP адрес'), + null=True, + blank=True + ) + user_agent = models.TextField(_('User Agent'), blank=True) + + # Дополнительные данные о действии (JSON) + metadata = models.JSONField( + _('Метаданные'), + null=True, + blank=True, + help_text=_('Дополнительная информация о действии') + ) + + class Meta: + verbose_name = _('Запись аудита') + verbose_name_plural = _('Записи аудита') + ordering = ['-timestamp'] + indexes = [ + models.Index(fields=['document', '-timestamp']), + models.Index(fields=['user', '-timestamp']), + models.Index(fields=['action', '-timestamp']), + ] + + def __str__(self): + return f'{self.user} {self.get_action_display()} {self.document} @ {self.timestamp:%d.%m.%Y %H:%M}' diff --git a/backend/search/search_indexes.py b/backend/search/search_indexes.py index bf43cfac..c876fba3 100644 --- a/backend/search/search_indexes.py +++ b/backend/search/search_indexes.py @@ -140,10 +140,30 @@ if Equipment: # 11. Документы (Documents) Document = get_model_safe("documents", "Document") if Document: + from watson.search import SearchAdapter + + class DocumentAdapter(SearchAdapter): + """Кастомный адаптер для Document с URL и расширенным поиском.""" + + def get_title(self, obj): + """Возвращает заголовок для результатов поиска.""" + return obj.title + + def get_description(self, obj): + """Возвращает описание для результатов поиска.""" + # Возвращаем первые 200 символов extracted_text или description + text = obj.extracted_text if obj.extracted_text else obj.description + return text[:200] + '...' if len(text) > 200 else text + + def get_url(self, obj): + """Возвращает URL документа.""" + return f"/documents/{obj.id}/" + register( Document, - fields=("title", "description"), - store=("title", "uploaded_at"), + adapter_cls=DocumentAdapter, + fields=("title", "description", "extracted_text"), # Добавлен extracted_text! + store=("title", "uploaded_at", "status"), ) # 12. Уведомления (Notifications) diff --git a/docs/in-progress/MAYAN_LEVEL_DMS_PLAN.md b/docs/in-progress/MAYAN_LEVEL_DMS_PLAN.md new file mode 100644 index 00000000..9a5af93c --- /dev/null +++ b/docs/in-progress/MAYAN_LEVEL_DMS_PLAN.md @@ -0,0 +1,385 @@ +# План реализации DMS уровня Mayan EDMS + +**Цель:** Создать корпоративную систему управления документами не ниже Mayan EDMS + +**Дата начала:** 28 февраля 2026 г. + +--- + +## 🎯 Ключевые возможности Mayan EDMS для реализации: + +1. ✅ **Document Storage** - django-filer + metadata +2. ✅ **Full-text Search** - django-watson + client-side +3. ✅ **OCR** - Tesseract.js (client-side) +4. ✅ **Versioning** - django-reversion +5. ✅ **Workflow** - django-fsm + визуализация +6. ✅ **Permissions** - django-rules + filer ACL +7. ✅ **Folders/Cabinets** - filer.Folder + Tags +8. ✅ **PDF Operations** - pdf-lib (client-side) +9. ✅ **Notifications** - существующая система +10. ✅ **REST API** - DRF +11. ✅ **Modern UI** - Next.js + React + +--- + +## 📋 PHASE 1: Backend Foundation (2-3 дня) + +### 1.1 Document Model Enhancement +- [x] Добавить поле `folder` (уже сделано) +- [ ] Добавить `extracted_text` для full-text search +- [ ] Добавить `document_type` FK +- [ ] Добавить `tags` M2M +- [ ] Интегрировать с django-watson +- [ ] Добавить django-reversion + +### 1.2 Additional Models +- [ ] `DocumentType` - типы документов с JSON schema метаданных +- [ ] `DocumentMetadata` - динамические key-value метаданные +- [ ] `DocumentTag` - теги для категоризации +- [ ] `Cabinet` - виртуальные хранилища (MPTT) +- [ ] `DocumentAuditLog` - полный аудит действий + +### 1.3 Serializers & API +- [ ] Расширить `DocumentReadSerializer` (folder, tags, metadata) +- [ ] Создать `FolderSerializer` для filer.Folder +- [ ] Создать `DocumentTypeSerializer` +- [ ] Создать `TagSerializer` +- [ ] API endpoint: `/folders/` (tree view) +- [ ] API endpoint: `/document-types/` +- [ ] API endpoint: `/tags/` +- [ ] API endpoint: `/documents/{id}/versions/` +- [ ] API endpoint: `/documents/{id}/audit-log/` + +### 1.4 File Browser UI Integration +- [ ] Подключить `filer.server.urls` для админки +- [ ] Настроить FILER_* settings +- [ ] Протестировать File Browser + +### 1.5 Search Integration +- [ ] Зарегистрировать Document в watson +- [ ] API endpoint: `/search/?q=...` +- [ ] Фильтры по типу, дате, статусу + +**Результат Phase 1:** Полноценный backend API с папками, поиском, версионированием + +--- + +## 📋 PHASE 2: Frontend Foundation (2-3 дня) + +### 2.1 Install Dependencies +```bash +cd frontend +npm install tesseract.js pdf-lib mammoth fuse.js reactflow recharts react-signature-canvas dnd-kit +``` + +### 2.2 Core Components Structure +``` +src/components/documents/ + ├── browser/ + │ ├── FolderTree.tsx # Древовидная навигация + │ ├── DocumentGrid.tsx # Grid view документов + │ ├── DocumentList.tsx # List view документов + │ └── Breadcrumbs.tsx # Навигация + ├── upload/ + │ ├── UploadZone.tsx # Drag & drop + processing + │ ├── ProcessingProgress.tsx # Status bar обработки + │ └── FilePreview.tsx # Preview перед загрузкой + ├── viewer/ + │ ├── PDFViewer.tsx # Улучшенный PDF viewer + │ ├── ImageViewer.tsx # Lightbox для изображений + │ └── DocumentPreview.tsx # Quick preview modal + ├── search/ + │ ├── SearchBar.tsx # Поиск с автодополнением + │ ├── SearchFilters.tsx # Фильтры + │ └── SearchResults.tsx # Результаты с подсветкой + └── metadata/ + ├── DocumentEditor.tsx # Форма редактирования + ├── MetadataForm.tsx # Динамические метаданные + └── TagsInput.tsx # Теги с автодополнением +``` + +### 2.3 API Client Extension +- [ ] Расширить `apiClient` методами для folders +- [ ] Методы для tags +- [ ] Методы для document types +- [ ] Методы для versions +- [ ] Methods для audit log + +### 2.4 TypeScript Types +- [ ] `Folder` interface +- [ ] `DocumentType` interface +- [ ] `Tag` interface +- [ ] `DocumentMetadata` interface +- [ ] `AuditLogEntry` interface + +**Результат Phase 2:** Базовый File Browser с навигацией и загрузкой + +--- + +## 📋 PHASE 3: Client-side Processing (2-3 дня) + +### 3.1 OCR Integration (Tesseract.js) +```typescript +// src/lib/ocr.ts +import Tesseract from 'tesseract.js'; + +export async function performOCR( + file: File, + onProgress?: (progress: number) => void +): Promise { + const { data: { text } } = await Tesseract.recognize( + file, + 'rus+eng', + { logger: m => onProgress?.(m.progress * 100) } + ); + return text; +} +``` + +### 3.2 PDF Text Extraction +```typescript +// src/lib/pdf-utils.ts +import * as pdfjsLib from 'pdfjs-dist'; + +export async function extractPDFText(file: File): Promise { + // ... implementation +} + +export async function generatePDFThumbnail(file: File): Promise { + // ... implementation +} +``` + +### 3.3 PDF Operations (pdf-lib) +```typescript +// src/lib/pdf-operations.ts +import { PDFDocument } from 'pdf-lib'; + +export async function splitPDF(file: File, pages: number[]): Promise +export async function mergePDFs(files: File[]): Promise +export async function rotatePDF(file: File, rotation: number): Promise +``` + +### 3.4 Image Processing +```typescript +// src/lib/image-utils.ts +export async function compressImage(file: File, maxSizeMB: number): Promise +export async function cropImage(file: File, area: CropArea): Promise +export async function rotateImage(file: File, degrees: number): Promise +``` + +### 3.5 DOCX Extraction +```typescript +// src/lib/docx-utils.ts +import mammoth from 'mammoth'; + +export async function extractDOCXText(file: File): Promise +``` + +### 3.6 Enhanced Upload Flow +- [ ] Pre-processing перед загрузкой +- [ ] Progress indicators для каждого этапа +- [ ] Preview extracted text +- [ ] Automatic title suggestion +- [ ] Validation на клиенте + +**Результат Phase 3:** Полная client-side обработка файлов + +--- + +## 📋 PHASE 4: Advanced Features (3-4 дня) + +### 4.1 Enhanced PDF Viewer +- [ ] Page navigation (prev/next) +- [ ] Zoom controls (fit-width, fit-page, custom) +- [ ] Rotation +- [ ] Thumbnail sidebar +- [ ] Page search +- [ ] Print functionality + +### 4.2 PDF Annotations +```typescript +// src/components/documents/viewer/PDFAnnotations.tsx +- [ ] Drawing tools (pen, highlighter, shapes) +- [ ] Text annotations +- [ ] Sticky notes +- [ ] Save/Load annotations +- [ ] Export annotated PDF +``` + +### 4.3 Advanced Search +- [ ] Fuzzy search (Fuse.js) +- [ ] Search в extracted_text с подсветкой +- [ ] Фильтры: дата, тип, автор, статус, теги +- [ ] Сохраненные поиски (localStorage) +- [ ] Recent searches + +### 4.4 Batch Operations +- [ ] Multiple select (checkboxes) +- [ ] Bulk move to folder +- [ ] Bulk add tags +- [ ] Bulk change status +- [ ] Bulk delete +- [ ] Progress indicator + +### 4.5 Dashboard & Analytics +```typescript +// src/app/documents/dashboard/page.tsx +- [ ] Статистика: всего документов, по типам, по статусам +- [ ] Recent uploads (chart) +- [ ] Top tags cloud +- [ ] Activity feed +- [ ] My documents / Favorites +``` + +**Результат Phase 4:** Rich UI с расширенными возможностями + +--- + +## 📋 PHASE 5: Workflow & Permissions (2-3 дня) + +### 5.1 Workflow Visualization +```typescript +// src/components/documents/workflow/WorkflowDiagram.tsx +- [ ] React Flow диаграмма states/transitions +- [ ] Highlight current state +- [ ] Show available transitions +- [ ] Timeline история переходов +``` + +### 5.2 Workflow Actions UI +- [ ] Кнопки transitions (уже есть базово) +- [ ] Modal с комментарием при переходе +- [ ] Assignee selection (кому назначить) +- [ ] Due date для review + +### 5.3 Permissions UI +```typescript +// src/components/documents/permissions/PermissionsEditor.tsx +- [ ] User/Group selector +- [ ] Permission checkboxes (view, download, edit, delete) +- [ ] Inheritance от папки +- [ ] ACL list display +``` + +### 5.4 Version History +```typescript +// src/components/documents/versions/VersionHistory.tsx +- [ ] Timeline версий +- [ ] Diff viewer (сравнение метаданных) +- [ ] Download старой версии +- [ ] Restore к предыдущей версии +``` + +### 5.5 Audit Log Viewer +```typescript +// src/components/documents/audit/AuditLog.tsx +- [ ] Timeline всех действий +- [ ] Filter: action type, user, date range +- [ ] Export audit log +``` + +**Результат Phase 5:** Полный workflow + права + аудит + +--- + +## 📋 PHASE 6: Polish & Production (2-3 дня) + +### 6.1 Digital Signatures +- [ ] Backend: создание/проверка подписи +- [ ] Frontend: signature pad component +- [ ] Certificate upload/management +- [ ] Visual indicator подписанных документов + +### 6.2 Performance Optimization +- [ ] Lazy loading компонентов +- [ ] Virtual scrolling для больших списков (@tanstack/react-virtual) +- [ ] Image lazy loading +- [ ] API response caching (React Query) +- [ ] Debouncing search + +### 6.3 Offline Support +- [ ] Service Worker +- [ ] IndexedDB для offline documents +- [ ] Sync queue для offline uploads + +### 6.4 Mobile Responsive +- [ ] Adaptive layout +- [ ] Touch gestures для PDF viewer +- [ ] Mobile upload (camera) + +### 6.5 Testing +- [ ] Backend: pytest для API +- [ ] Frontend: Jest + React Testing Library +- [ ] E2E: Playwright + +### 6.6 Documentation +- [ ] API documentation (Swagger/ReDoc) +- [ ] User guide +- [ ] Admin guide +- [ ] Developer guide + +**Результат Phase 6:** Production-ready система + +--- + +## 🔄 Итеративная разработка: + +Каждая фаза: +1. **Разработка** функциональности +2. **Тестирование** +3. **Документирование** +4. **Коммит** в git +5. **Демо** пользователю + +--- + +## 📊 Метрики успеха (сравнение с Mayan EDMS): + +| Функция | Mayan EDMS | Наша система | Статус | +|---------|------------|--------------|--------| +| Document Storage | ✅ | 🔄 In progress | Phase 1 | +| Folders/Cabinets | ✅ | 🔄 In progress | Phase 1 | +| OCR | ✅ (server) | ✅ (client) | Phase 3 | +| Full-text Search | ✅ | 🔄 In progress | Phase 1 | +| Workflow | ✅ | 🔄 In progress | Phase 5 | +| Permissions | ✅ | 🔄 In progress | Phase 5 | +| Versioning | ✅ | 🔄 In progress | Phase 1 | +| Tags/Metadata | ✅ | 🔄 In progress | Phase 1 | +| Digital Signatures | ✅ | ⏳ Planned | Phase 6 | +| REST API | ✅ | 🔄 In progress | Phase 1 | +| Modern UI | ❌ | ✅ | Phase 2-4 | +| Client-side Processing | ❌ | ✅ | Phase 3 | +| PDF Viewer | ✅ | ✅ (better) | Phase 4 | +| Annotations | ✅ | ⏳ Planned | Phase 4 | +| Dashboard | ✅ | ⏳ Planned | Phase 4 | + +**Преимущества над Mayan:** +- ✅ Современный React UI вместо Django templates +- ✅ Client-side processing (быстрее, не нагружает сервер) +- ✅ TypeScript безопасность +- ✅ Better UX (real-time feedback, progressive enhancement) + +--- + +## ⏱️ Общая оценка времени: + +- **Phase 1:** 2-3 дня (Backend Foundation) +- **Phase 2:** 2-3 дня (Frontend Foundation) +- **Phase 3:** 2-3 дня (Client-side Processing) +- **Phase 4:** 3-4 дня (Advanced Features) +- **Phase 5:** 2-3 дня (Workflow & Permissions) +- **Phase 6:** 2-3 дня (Polish & Production) + +**Total: 13-19 дней чистой разработки** + +--- + +## 🚀 Начинаем с Phase 1! + +Следующие шаги: +1. ✅ Расширить модель Document +2. ✅ Создать дополнительные модели +3. ✅ Интегрировать django-watson +4. ✅ Создать API endpoints +5. ✅ Подключить File Browser