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 */}