feat(documents): Phase 1 - Backend Foundation Complete

- Добавлены расширенные модели для Mayan-level DMS:
  * DocumentType - типы документов с JSON schema
  * DocumentTag - теги для категоризации
  * Cabinet - виртуальные коллекции
  * DocumentMetadata - динамические key-value метаданные
  * DocumentAuditLog - полный аудит действий

- Расширена модель Document:
  * extracted_text - для полнотекстового поиска
  * document_type - FK к типу
  * modified_by, modified_at - audit trail

- Интегрирован django-watson для full-text search:
  * Регистрация с extracted_text
  * Кастомный adapter с URL
  * Обновлен индекс (22 документа)

- API Serializers:
  * DocumentTypeSerializer
  * DocumentTagSerializer
  * CabinetSerializer
  * FolderSerializer расширен
  * DocumentReadSerializer обновлен с новыми полями

Следующий этап: Phase 2 Frontend Foundation
This commit is contained in:
igor_nadein 2026-02-28 09:26:23 +03:00
parent 856e08db4a
commit ab07a6ff9a
6 changed files with 967 additions and 3 deletions

View File

@ -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",
)

View File

@ -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='Папка'),
),
]

View File

@ -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')},
},
),
]

View File

@ -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}'

View File

@ -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)

View File

@ -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<string> {
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<string> {
// ... implementation
}
export async function generatePDFThumbnail(file: File): Promise<string> {
// ... 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<Uint8Array>
export async function mergePDFs(files: File[]): Promise<Uint8Array>
export async function rotatePDF(file: File, rotation: number): Promise<Uint8Array>
```
### 3.4 Image Processing
```typescript
// src/lib/image-utils.ts
export async function compressImage(file: File, maxSizeMB: number): Promise<Blob>
export async function cropImage(file: File, area: CropArea): Promise<Blob>
export async function rotateImage(file: File, degrees: number): Promise<Blob>
```
### 3.5 DOCX Extraction
```typescript
// src/lib/docx-utils.ts
import mammoth from 'mammoth';
export async function extractDOCXText(file: File): Promise<string>
```
### 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