From 11acea9ad05d469232d7d5b3977065b09d330f97 Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Sat, 28 Feb 2026 14:04:27 +0300 Subject: [PATCH] feat: Extend DocumentUploadForm with full enterprise features PROBLEM: - Upload form was minimal, not using full backend capabilities - Missing: recipients, departments, acknowledgement requirement - No folder indication when uploading to specific folder SOLUTION - BACKEND: - Added 'acknowledgement_required' field to DocumentWriteSerializer - Now accepts: sent_to_all, department_ids, recipient_ids, acknowledgement_required SOLUTION - FRONTEND: Extended DocumentUploadForm with enterprise-grade features: NEW FIELDS: 1. Sent to All Toggle - Checkbox to send document to all employees - When checked, hides recipients/departments selection 2. Recipients Section (when sent_to_all = false) - Multi-select for departments (with Building2 icon) - Multi-select for specific employees (with Users icon) - Live loading from API - Validation: requires at least one department OR recipient - Helper text for Ctrl/Cmd multi-select 3. Acknowledgement Required - Checkbox to require recipients to acknowledge reading - Clear explanation of what it means UX IMPROVEMENTS: - Folder indicator: shows when uploading to specific folder - Visual sections with dividers and headers - Icons for each field type - Warning when no recipients selected (amber alert) - Better validation messages - Reset all new fields on successful upload API CLIENT: - Updated createDocument signature to include acknowledgement_required - Properly serializes checkbox values as strings DATA LOADING: - useEffect loads departments and employees on mount - Loading states during fetch - Handles empty lists gracefully VALIDATION: - Requires file and title (unchanged) - NEW: When sent_to_all=false, requires departments OR recipients - Shows user-friendly error messages BENEFITS: - Full feature parity with backend capabilities - Professional document management workflow - Granular control over document distribution - Acknowledgement tracking from upload stage - Better UX with progressive disclosure --- backend/api/v1/documents/serializers.py | 1 + .../documents/DocumentUploadForm.tsx | 221 +++++++++++++++++- frontend/src/lib/api.ts | 6 + 3 files changed, 224 insertions(+), 4 deletions(-) diff --git a/backend/api/v1/documents/serializers.py b/backend/api/v1/documents/serializers.py index 6ad2e103..bfdc12a8 100644 --- a/backend/api/v1/documents/serializers.py +++ b/backend/api/v1/documents/serializers.py @@ -282,6 +282,7 @@ class DocumentWriteSerializer(serializers.ModelSerializer): "file", "extracted_text", "sent_to_all", + "acknowledgement_required", "department_ids", "recipient_ids", ) diff --git a/frontend/src/components/documents/DocumentUploadForm.tsx b/frontend/src/components/documents/DocumentUploadForm.tsx index ec2a0cff..2b5e80f9 100644 --- a/frontend/src/components/documents/DocumentUploadForm.tsx +++ b/frontend/src/components/documents/DocumentUploadForm.tsx @@ -1,12 +1,35 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect } from "react"; import { useDropzone } from "react-dropzone"; -import { Upload, X, FileText, AlertCircle, Loader2, FolderOpen } from "lucide-react"; +import { + Upload, + X, + FileText, + AlertCircle, + Loader2, + FolderOpen, + Users, + Building2, + CheckCircle, + Tag as TagIcon, +} from "lucide-react"; import { apiClient } from "@/lib/api"; import { toast } from "sonner"; import { processDocument, needsProcessing, type ProcessingProgress } from "@/lib/document-utils"; +interface Department { + id: number; + name: string; +} + +interface User { + id: number; + first_name: string; + last_name: string; + email?: string; +} + interface DocumentUploadFormProps { onSuccess?: () => void; onCancel?: () => void; @@ -33,6 +56,45 @@ export function DocumentUploadForm({ onSuccess, onCancel, currentFolderId }: Doc const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + // New fields for extended functionality + const [sentToAll, setSentToAll] = useState(true); + const [acknowledgementRequired, setAcknowledgementRequired] = useState(false); + const [selectedDepartments, setSelectedDepartments] = useState([]); + const [selectedRecipients, setSelectedRecipients] = useState([]); + + // Data for selects + const [departments, setDepartments] = useState([]); + const [employees, setEmployees] = useState([]); + const [loadingDepartments, setLoadingDepartments] = useState(false); + const [loadingEmployees, setLoadingEmployees] = useState(false); + + // Load departments and employees + useEffect(() => { + const loadData = async () => { + try { + setLoadingDepartments(true); + const deptResponse = await apiClient.getDepartments({ limit: 1000 }); + setDepartments(deptResponse.results || deptResponse); + } catch (err) { + console.error("Ошибка загрузки отделов:", err); + } finally { + setLoadingDepartments(false); + } + + try { + setLoadingEmployees(true); + const empResponse = await apiClient.getEmployees({ limit: 1000 }); + setEmployees(empResponse.results || empResponse); + } catch (err) { + console.error("Ошибка загрузки сотрудников:", err); + } finally { + setLoadingEmployees(false); + } + }; + + loadData(); + }, []); + const onDrop = useCallback(async (acceptedFiles: File[]) => { if (acceptedFiles.length > 0) { const selectedFile = acceptedFiles[0]; @@ -110,6 +172,12 @@ export function DocumentUploadForm({ onSuccess, onCancel, currentFolderId }: Doc return; } + // Validate recipients if sent_to_all is false + if (!sentToAll && selectedDepartments.length === 0 && selectedRecipients.length === 0) { + setError("Укажите получателей или отделы, либо выберите 'Отправить всем'"); + return; + } + setIsSubmitting(true); try { @@ -120,8 +188,12 @@ export function DocumentUploadForm({ onSuccess, onCancel, currentFolderId }: Doc title, description, file: fileToUpload, - extracted_text: extractedText || undefined, // Отправляем извлеченный текст, если есть - folder_id: currentFolderId || undefined, // Сохраняем в текущей папке если она выбрана + extracted_text: extractedText || undefined, + folder_id: currentFolderId || undefined, + sent_to_all: sentToAll, + department_ids: sentToAll ? undefined : selectedDepartments, + recipient_ids: sentToAll ? undefined : selectedRecipients, + acknowledgement_required: acknowledgementRequired, }); toast.success("Документ успешно загружен"); @@ -132,6 +204,10 @@ export function DocumentUploadForm({ onSuccess, onCancel, currentFolderId }: Doc setFile(null); setProcessedFile(null); setExtractedText(""); + setSentToAll(true); + setAcknowledgementRequired(false); + setSelectedDepartments([]); + setSelectedRecipients([]); if (onSuccess) { onSuccess(); @@ -297,6 +373,143 @@ export function DocumentUploadForm({ onSuccess, onCancel, currentFolderId }: Doc )} + {/* Divider */} +
+

Настройки доступа и уведомлений

+
+ + {/* Sent to All Toggle */} +
+ { + setSentToAll(e.target.checked); + if (e.target.checked) { + setSelectedDepartments([]); + setSelectedRecipients([]); + } + }} + className="mt-0.5 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-2 focus:ring-sky-100" + /> +
+ +

+ Документ будет доступен всем активным сотрудникам +

+
+
+ + {/* Recipients section - only show when sentToAll is false */} + {!sentToAll && ( +
+
+ + Выберите получателей +
+ + {/* Departments */} +
+ + +

+ Удерживайте Ctrl/Cmd для выбора нескольких +

+
+ + {/* Recipients */} +
+ + +

+ Удерживайте Ctrl/Cmd для выбора нескольких +

+
+ + {selectedDepartments.length === 0 && selectedRecipients.length === 0 && ( +
+ + Выберите хотя бы один отдел или сотрудника +
+ )} +
+ )} + + {/* Acknowledgement Required */} +
+ setAcknowledgementRequired(e.target.checked)} + className="mt-0.5 h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-2 focus:ring-sky-100" + /> +
+ +

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

+
+
+ {/* Buttons */}