Files
ctms/docs/plans/2026-05-27-etmf-implementation.md
2026-06-23 09:21:57 +08:00

386 lines
8.2 KiB
Markdown

# eTMF Module Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build an eTMF module that combines a compliant TMF directory tree with reusable document version management.
**Architecture:** Add eTMF directory nodes as metadata, then mount existing `documents` records onto those nodes with `etmf_node_id`. Keep file storage, versions, permissions, downloads, distribution, and audit behavior in the existing document module.
**Tech Stack:** FastAPI, SQLAlchemy async ORM, Alembic, PostgreSQL, Vue 3, Element Plus, Vitest, Pytest.
---
## Implementation Notes
The repository instruction says not to plan or execute git commits unless explicitly requested. This plan intentionally omits commit steps.
Follow TDD for each backend behavior before implementation. Keep the first release small: no e-signature, no workflow redesign, no template versioning, no bulk import.
### Task 1: Backend eTMF Node Model
**Files:**
- Create: `backend/app/models/etmf.py`
- Create: `backend/app/schemas/etmf.py`
- Modify: `backend/app/db/base.py`
- Create: `backend/alembic/versions/20260527_07_add_etmf_nodes.py`
- Test: `backend/tests/test_etmf_nodes.py`
**Step 1: Write failing model/import test**
Add a test that imports `EtmfNode` through `app.db.base` metadata and verifies the table name is registered.
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: fail because `app.models.etmf` does not exist.
**Step 2: Add model**
Create `EtmfNode` with these columns:
- `id`
- `study_id`
- `parent_id`
- `code`
- `name`
- `description`
- `scope_type`
- `required`
- `expected_doc_type`
- `sort_order`
- `is_active`
- `created_at`
- `updated_at`
Use existing enum values from document scope where practical. Keep node-specific logic out of the model.
**Step 3: Add schemas**
Create request and response schemas:
- `EtmfNodeCreate`
- `EtmfNodeUpdate`
- `EtmfNodeRead`
- `EtmfTreeNode`
- `EtmfNodeStatusSummary`
**Step 4: Add migration**
Create `etmf_nodes` with:
- Foreign key to `studies.id`
- Self foreign key `parent_id`
- Unique constraint on `(study_id, parent_id, code)`
- Indexes on `study_id`, `parent_id`, `scope_type`, `is_active`
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: pass.
### Task 2: Link Documents To eTMF Nodes
**Files:**
- Modify: `backend/app/models/document.py`
- Modify: `backend/app/schemas/document.py`
- Modify: `backend/app/crud/document.py`
- Modify: `backend/app/services/document_service.py`
- Modify: `frontend/src/types/documents.ts`
- Modify: `backend/alembic/versions/20260527_07_add_etmf_nodes.py`
- Test: `backend/tests/test_etmf_nodes.py`
**Step 1: Write failing service test**
Test that creating a document with `etmf_node_id` persists the relationship and list/detail responses include it.
Expected: fail because schema and model do not include `etmf_node_id`.
**Step 2: Add model column**
Add nullable `Document.etmf_node_id` with foreign key to `etmf_nodes.id`.
**Step 3: Add schema fields**
Add `etmf_node_id` to:
- `DocumentCreate`
- `DocumentUpdate`
- `DocumentSummary`
- `DocumentDetail`
**Step 4: Validate node scope**
In `document_service.create_document`, if `etmf_node_id` is provided:
- Verify node exists.
- Verify node belongs to the same study.
- If node scope is `SITE`, require `site_id`.
- If node scope is `GLOBAL`, normalize `site_id` to null unless the product decision changes.
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_nodes.py -q
```
Expected: pass.
### Task 3: eTMF Service And Tree API
**Files:**
- Create: `backend/app/crud/etmf.py`
- Create: `backend/app/services/etmf_service.py`
- Create: `backend/app/api/v1/etmf.py`
- Modify: `backend/app/api/v1/router.py`
- Test: `backend/tests/test_etmf_api.py`
**Step 1: Write failing API tests**
Cover:
- `GET /api/v1/etmf/tree?study_id=...`
- Required empty node returns `MISSING`.
- Node with document but no effective version returns `UPLOADED`.
- Node with current effective version returns `EFFECTIVE`.
- Site-scoped node can return per-site status.
Expected: fail because API does not exist.
**Step 2: Implement CRUD**
Add small CRUD functions:
- `get_node`
- `list_nodes_by_study`
- `create_node`
- `update_node`
Avoid broad generic repository abstractions.
**Step 3: Implement service**
Add tree assembly and status calculation:
- Build parent-child tree in memory.
- Query documents for target study and group by `etmf_node_id`.
- Calculate node status from required flag and document effective version presence.
- Keep status calculation deterministic and isolated for unit tests.
**Step 4: Implement routes**
Add:
- `GET /tree`
- `GET /nodes/{node_id}/documents`
- `POST /nodes`
- `PATCH /nodes/{node_id}`
- `POST /nodes/{node_id}/documents`
Use existing project permission checks through the document permission model.
**Step 5: Verify**
Run:
```bash
cd backend
pytest tests/test_etmf_api.py -q
```
Expected: pass.
### Task 4: Frontend API And Types
**Files:**
- Create: `frontend/src/types/etmf.ts`
- Create: `frontend/src/api/etmf.ts`
- Test: `frontend/src/api/etmf.test.ts`
**Step 1: Write failing API test**
Test that each frontend API helper calls the expected URL and HTTP method.
Expected: fail because `src/api/etmf.ts` does not exist.
**Step 2: Add types**
Add types matching backend schemas:
- `EtmfNodeStatus`
- `EtmfTreeNode`
- `EtmfNodeCreatePayload`
- `EtmfNodeUpdatePayload`
**Step 3: Add API helpers**
Add:
- `fetchEtmfTree`
- `fetchEtmfNodeDocuments`
- `createEtmfNode`
- `updateEtmfNode`
- `createEtmfDocument`
**Step 4: Verify**
Run:
```bash
cd frontend
npm test -- src/api/etmf.test.ts
```
Expected: pass.
### Task 5: eTMF Page
**Files:**
- Replace: `frontend/src/views/ia/EtmfPlaceholder.vue`
- Modify: `frontend/src/locales/zh-CN.ts`
**Step 1: Write failing page test**
Test that `/etmf` page:
- Renders tree panel.
- Loads eTMF tree for current study.
- Selecting a node loads its documents.
- Shows missing/effective status labels.
Expected: fail because the page is still a placeholder.
**Step 2: Build layout**
Use existing CTMS shell classes:
- Top action bar.
- Left tree panel.
- Main document table.
- Right node detail panel.
Do not introduce a marketing-style page or nested card layout.
**Step 3: Add interactions**
Implement:
- Center filter.
- Node selection.
- Refresh.
- Create node dialog.
- Create document dialog that posts to eTMF node document API.
Reuse existing display helpers and document enums.
**Step 4: Verify**
Run:
```bash
cd frontend
npm test -- src/api/etmf.test.ts
```
Expected: pass.
### Task 6: Route And Permission Regression
**Files:**
- Modify: `frontend/src/utils/projectRoutePermissions.ts`
- Modify: `frontend/src/utils/projectRoutePermissions.test.ts`
- Modify: `backend/tests/test_api_permissions.py`
**Step 1: Write failing regression tests**
Ensure:
- `/etmf` still requires `documents:read`.
- eTMF write endpoints are covered by document permissions.
- Existing document permission tests remain stable.
**Step 2: Implement minimal permission wiring**
Keep first-stage eTMF permissions mapped to existing document operation keys.
**Step 3: Verify**
Run:
```bash
cd backend
pytest tests/test_api_permissions.py -q
cd ../frontend
npm test -- src/utils/projectRoutePermissions.test.ts
```
Expected: pass.
### Task 7: End-To-End Verification
**Files:**
- No source changes expected unless verification finds a bug.
**Step 1: Run backend focused tests**
```bash
cd backend
pytest tests/test_etmf_nodes.py tests/test_etmf_api.py tests/test_api_permissions.py -q
```
Expected: pass.
**Step 2: Run frontend focused tests**
```bash
cd frontend
npm test -- src/api/etmf.test.ts src/utils/projectRoutePermissions.test.ts
```
Expected: pass.
**Step 3: Run UI contract check if available**
```bash
cd frontend
npm run verify-ui-contract
```
Expected: pass.
**Step 4: Manual browser check**
Start the dev server and open `/etmf` with an active study:
```bash
cd frontend
npm run dev
```
Expected:
- eTMF page is not blank.
- Directory tree is visible.
- Selecting a node updates the document list.
- Center filter does not break layout.
- Text does not overlap at desktop and mobile widths.