Corp/frontend/src/components/documents/DocumentWorkflowButtons.tsx
igor_nadein 12f3b7b81b feat(frontend): Полная интеграция документов с django-filer и django-fsm
- Установлены библиотеки: 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
2026-02-28 08:30:11 +03:00

169 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import {
Send,
CheckCircle,
XCircle,
Globe,
RotateCcw,
Archive,
ArchiveRestore
} from "lucide-react";
import { apiClient } from "@/lib/api";
import { toast } from "sonner";
import type { DocumentStatus } from "@/types/api";
interface DocumentWorkflowButtonsProps {
documentId: number;
currentStatus: DocumentStatus;
onStatusChange?: () => void;
}
interface WorkflowAction {
label: string;
icon: React.ReactNode;
action: () => Promise<any>;
confirmMessage?: string;
successMessage: string;
variant: "primary" | "success" | "danger" | "secondary";
}
export function DocumentWorkflowButtons({
documentId,
currentStatus,
onStatusChange,
}: DocumentWorkflowButtonsProps) {
const [isLoading, setIsLoading] = useState(false);
const handleAction = async (action: WorkflowAction) => {
if (action.confirmMessage && !window.confirm(action.confirmMessage)) {
return;
}
setIsLoading(true);
try {
await action.action();
toast.success(action.successMessage);
if (onStatusChange) {
onStatusChange();
}
} catch (err) {
console.error("Ошибка изменения статуса:", err);
toast.error("Не удалось изменить статус документа");
} finally {
setIsLoading(false);
}
};
const getAvailableActions = (): WorkflowAction[] => {
const actions: WorkflowAction[] = [];
switch (currentStatus) {
case "draft":
actions.push({
label: "Отправить на рассмотрение",
icon: <Send size={14} />,
action: () => apiClient.submitDocumentForReview(documentId),
successMessage: "Документ отправлен на рассмотрение",
variant: "primary",
});
break;
case "in_review":
actions.push(
{
label: "Утвердить",
icon: <CheckCircle size={14} />,
action: () => apiClient.approveDocument(documentId),
confirmMessage: "Вы уверены, что хотите утвердить этот документ?",
successMessage: "Документ утвержден",
variant: "success",
},
{
label: "Отклонить",
icon: <XCircle size={14} />,
action: () => apiClient.rejectDocument(documentId),
confirmMessage: "Вы уверены, что хотите отклонить этот документ?",
successMessage: "Документ отклонен",
variant: "danger",
},
{
label: "Вернуть в черновик",
icon: <RotateCcw size={14} />,
action: () => apiClient.returnDocumentToDraft(documentId),
successMessage: "Документ возвращен в черновик",
variant: "secondary",
}
);
break;
case "approved":
actions.push({
label: "Опубликовать",
icon: <Globe size={14} />,
action: () => apiClient.publishDocument(documentId),
confirmMessage: "Опубликовать этот документ для всех пользователей?",
successMessage: "Документ опубликован",
variant: "primary",
});
break;
case "published":
actions.push({
label: "Архивировать",
icon: <Archive size={14} />,
action: () => apiClient.archiveDocument(documentId),
confirmMessage: "Архивировать этот документ?",
successMessage: "Документ архивирован",
variant: "secondary",
});
break;
case "archived":
actions.push({
label: "Разархивировать",
icon: <ArchiveRestore size={14} />,
action: () => apiClient.unarchiveDocument(documentId),
successMessage: "Документ разархивирован",
variant: "primary",
});
break;
}
return actions;
};
const actions = getAvailableActions();
if (actions.length === 0) {
return null;
}
const variantClasses = {
primary: "bg-sky-600 hover:bg-sky-700 text-white",
success: "bg-green-600 hover:bg-green-700 text-white",
danger: "bg-red-600 hover:bg-red-700 text-white",
secondary: "bg-gray-200 hover:bg-gray-300 text-gray-800",
};
return (
<div className="flex flex-wrap gap-2">
{actions.map((action, index) => (
<button
key={index}
type="button"
onClick={() => handleAction(action)}
disabled={isLoading}
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition disabled:cursor-not-allowed disabled:opacity-50 ${
variantClasses[action.variant]
}`}
>
{action.icon}
{action.label}
</button>
))}
</div>
);
}