diff --git a/backend/app/api/v1/documents.py b/backend/app/api/v1/documents.py index 89202e48..be31edec 100644 --- a/backend/app/api/v1/documents.py +++ b/backend/app/api/v1/documents.py @@ -9,7 +9,7 @@ from app.core.deps import get_current_user, get_db_session from app.schemas.acknowledgement import AcknowledgementCreate, AcknowledgementRead from app.schemas.common import PaginatedResponse from app.schemas.distribution import DistributionCreate, DistributionRead -from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary +from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary, DocumentUpdate from app.schemas.document_version import DocumentVersionRead from app.services import document_service from app.utils.pagination import paginate @@ -66,6 +66,17 @@ async def get_document_detail( return await document_service.get_document_detail(db, document_id, current_user) +@router.patch("/documents/{document_id}", response_model=DocumentSummary) +async def update_document( + document_id: uuid.UUID, + payload: DocumentUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> DocumentSummary: + doc = await document_service.update_document(db, document_id, payload, current_user) + return DocumentSummary.model_validate(doc) + + @router.delete("/documents/{document_id}", response_model=DocumentSummary) async def delete_document( document_id: uuid.UUID, diff --git a/backend/app/services/document_service.py b/backend/app/services/document_service.py index a63854cd..b6cdf5e1 100644 --- a/backend/app/services/document_service.py +++ b/backend/app/services/document_service.py @@ -32,7 +32,7 @@ from app.models.document import Document, DocumentScopeType, DocumentStatus from app.models.document_version import DocumentVersion, DocumentVersionStatus from app.schemas.acknowledgement import AcknowledgementCreate from app.schemas.distribution import DistributionCreate, DistributionRead, DistributionStats -from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary +from app.schemas.document import DocumentCreate, DocumentDetail, DocumentSummary, DocumentUpdate from app.schemas.document_version import DocumentVersionRead, DocumentVersionSummary from app.schemas.notification import NotificationItem from app.schemas.user import UserDisplay @@ -43,6 +43,7 @@ DOCUMENT_ACTION_PERMISSIONS = { "view": "documents:read", "ack": "documents:read", "create_document": "documents:create", + "update_document": "documents:update", "create_version": "documents:update", "distribute": "documents:update", "delete_document": "documents:delete", @@ -261,6 +262,56 @@ async def get_document_detail( ) +async def update_document( + db: AsyncSession, + document_id: uuid.UUID, + doc_in: DocumentUpdate, + current_user, +) -> Document: + doc = await document_crud.get(db, document_id) + if not doc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文档不存在") + await _ensure_study_access(db, doc.trial_id, current_user, action="update_document") + + values = doc_in.model_dump(exclude_unset=True) + if not values: + return doc + + next_scope = values.get("scope_type", doc.scope_type) + next_scope_value = getattr(next_scope, "value", next_scope) + next_site_id = values.get("site_id", doc.site_id) + if next_scope_value == DocumentScopeType.SITE.value: + if not next_site_id: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="分中心文件需指定分中心") + site = await site_crud.get_site(db, next_site_id) + if not site or site.study_id != doc.trial_id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="分中心不属于当前项目") + if not site.is_active: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="中心已停用") + else: + values["site_id"] = None + + before = _doc_snapshot(doc) + await document_crud.update(db, document_id, values, commit=False) + updated = await document_crud.get(db, document_id) + if not updated: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文档不存在") + db.add( + AuditLog( + study_id=updated.trial_id, + entity_type="DOCUMENT", + entity_id=updated.id, + action="DOCUMENT_UPDATED", + detail=_audit_detail(before, _doc_snapshot(updated)), + operator_id=current_user.id, + operator_role=await get_operator_role_label(db, updated.trial_id, current_user), + ) + ) + await db.commit() + await db.refresh(updated) + return updated + + async def create_version( db: AsyncSession, document_id: uuid.UUID, diff --git a/frontend/src/api/documents.ts b/frontend/src/api/documents.ts index 1e7dcc92..2feafddd 100644 --- a/frontend/src/api/documents.ts +++ b/frontend/src/api/documents.ts @@ -1,4 +1,4 @@ -import { apiDelete, apiGet, apiPost } from "./axios"; +import { apiDelete, apiGet, apiPatch, apiPost } from "./axios"; import type { ApiListResponse } from "../types/api"; import type { Acknowledgement, @@ -14,6 +14,9 @@ export const fetchDocuments = (params: Record) => export const createDocument = (payload: Record) => apiPost("/api/v1/documents", payload); +export const updateDocument = (documentId: string, payload: Record) => + apiPatch(`/api/v1/documents/${documentId}`, payload); + export const deleteDocument = (documentId: string) => apiDelete(`/api/v1/documents/${documentId}`); diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 7156c5b8..6a9505b3 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -120,7 +120,7 @@
-