未知(继上次中断)
This commit is contained in:
@@ -27,7 +27,12 @@
|
||||
<el-descriptions :column="3" border class="ctms-descriptions">
|
||||
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.docType">{{ displayText(detail.doc_type, TEXT.enums.documentType) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.fields.site">{{ displaySite(detail.site_id) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.labels.currentEffective">{{ detail.current_effective_version?.version_no || TEXT.common.fallback }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.labels.currentEffective">
|
||||
{{ currentEffectiveVersion?.version_no || TEXT.common.fallback }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本日期">
|
||||
{{ formatDateOnly(currentEffectiveVersion?.effective_at || currentEffectiveVersion?.created_at || null) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="TEXT.modules.fileVersionManagement.columns.updatedAt">{{ formatDate(detail.updated_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
@@ -35,7 +40,7 @@
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="ctms-tabs">
|
||||
<el-tab-pane :label="TEXT.modules.fileVersionManagement.tabs.versions" name="versions">
|
||||
<div class="ctms-section-header">
|
||||
<div class="ctms-section-title">{{ TEXT.modules.fileVersionManagement.labels.versionTimeline }}</div>
|
||||
<div class="ctms-section-title"> </div>
|
||||
<div class="ctms-section-actions">
|
||||
<el-button type="primary" @click="openUpload" v-if="canAction('create_version')">
|
||||
<el-icon class="el-icon--left"><Upload /></el-icon>
|
||||
@@ -50,9 +55,9 @@
|
||||
<span class="font-medium">V{{ row.version_no }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.status" width="120">
|
||||
<el-table-column label="版本日期" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getVersionStatusType(row.status)" effect="light" size="small">{{ displayEnum(TEXT.enums.documentVersionStatus, row.status) }}</el-tag>
|
||||
<span class="text-secondary">{{ formatDateOnly(row.effective_at || row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="change_summary" :label="TEXT.modules.fileVersionManagement.fields.changeSummary" min-width="200" show-overflow-tooltip />
|
||||
@@ -61,37 +66,29 @@
|
||||
<span>{{ displayCreator(row.created_by) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.fileVersionManagement.labels.createdAt" width="180">
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.fields.parentVersion" width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ formatDate(row.created_at) }}</span>
|
||||
<span class="text-secondary">{{ displayParentVersion(row.parent_version_id) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.approvalSummary" min-width="220">
|
||||
<el-table-column prop="effective_at" :label="TEXT.modules.fileVersionManagement.columns.effectiveAt" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="text-xs text-secondary lh-sm">
|
||||
<div v-if="row.submitted_at">{{ TEXT.modules.fileVersionManagement.labels.submitAt }}:{{ formatDate(row.submitted_at) }}</div>
|
||||
<div v-if="row.approved_at" class="mt-1">
|
||||
{{ TEXT.modules.fileVersionManagement.labels.approveAt }}:{{ formatDate(row.approved_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-secondary">{{ formatDate(row.effective_at || row.created_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="280" align="center">
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="downloadVersion(row)" v-if="canAction('view')">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.download }}
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="openSubmit(row)" v-if="canAction('submit') && row.status === 'DRAFT'">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.submit }}
|
||||
</el-button>
|
||||
<el-button link type="success" size="small" @click="approve(row)" v-if="canAction('approve') && row.status === 'SUBMITTED'">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.approve }}
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="reject(row)" v-if="canAction('approve') && row.status === 'SUBMITTED'">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.reject }}
|
||||
</el-button>
|
||||
<el-button link type="warning" size="small" @click="makeEffective(row)" v-if="canAction('approve') && row.status === 'APPROVED'">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.makeEffective }}
|
||||
<el-button
|
||||
v-if="isAdmin"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="confirmDeleteVersion(row)"
|
||||
>
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -101,9 +98,9 @@
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.fileVersionManagement.tabs.distributions" name="distributions">
|
||||
<div class="ctms-section-header">
|
||||
<div class="ctms-section-title">{{ TEXT.modules.fileVersionManagement.labels.distributionList }}</div>
|
||||
<div class="ctms-section-title"> </div>
|
||||
<div class="ctms-section-actions">
|
||||
<el-button type="primary" @click="openDistribute" v-if="canAction('distribute')">
|
||||
<el-button type="primary" @click="openDistribute">
|
||||
<el-icon class="el-icon--left"><Share /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.actions.distribute }}
|
||||
</el-button>
|
||||
@@ -121,48 +118,7 @@
|
||||
<span class="font-medium">{{ displayTarget(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.ackStats" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span>{{ TEXT.modules.fileVersionManagement.actions.ackReceived }}: {{ row.stats?.received || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.overdue" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.stats?.overdue" type="danger" effect="dark" size="small">{{ TEXT.modules.fileVersionManagement.labels.overdue }}</el-tag>
|
||||
<span v-else class="text-secondary">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.dueAt" width="160">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-danger': row.stats?.overdue }">{{ formatDate(row.due_at) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.labels.ackAction" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="ack(row, 'RECEIVED')" v-if="canAction('ack')">
|
||||
{{ TEXT.modules.fileVersionManagement.actions.ackReceived }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="TEXT.modules.fileVersionManagement.tabs.audit" name="audit">
|
||||
<el-card class="ctms-table-card">
|
||||
<el-table :data="auditLogs" v-loading="auditLoading" class="ctms-table">
|
||||
<el-table-column prop="action" :label="TEXT.modules.fileVersionManagement.labels.auditAction" width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="font-medium">{{ row.action }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" :label="TEXT.modules.fileVersionManagement.labels.auditDetail" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="operator_role" :label="TEXT.common.labels.role" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain" size="small">{{ row.operator_role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" :label="TEXT.modules.fileVersionManagement.labels.auditTime" width="180">
|
||||
<el-table-column label="抄送时间" width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ formatDate(row.created_at) }}</span>
|
||||
</template>
|
||||
@@ -170,6 +126,7 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="uploadVisible" :title="TEXT.modules.fileVersionManagement.dialog.uploadTitle" width="520px" destroy-on-close class="ctms-dialog">
|
||||
@@ -177,6 +134,15 @@
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.versionNo" prop="version_no">
|
||||
<el-input v-model="uploadForm.version_no" :placeholder="TEXT.modules.fileVersionManagement.placeholders.versionNo" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本日期" prop="version_date">
|
||||
<el-date-picker
|
||||
v-model="uploadForm.version_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.changeSummary">
|
||||
<el-input v-model="uploadForm.change_summary" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
@@ -204,49 +170,44 @@
|
||||
<el-form :model="distributeForm" label-width="100px" class="ctms-form">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.targetList">
|
||||
<div class="target-list w-full">
|
||||
<div class="target-row" v-for="(item, index) in distributeForm.targets" :key="item.uid">
|
||||
<el-select
|
||||
v-model="item.target_type"
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
style="width: 120px"
|
||||
@change="onTargetTypeChange(index)"
|
||||
>
|
||||
<div class="target-toolbar">
|
||||
<el-select v-model="distributeForm.target_type" class="target-type-preset" :placeholder="TEXT.common.labels.all" @change="onTargetTypeChange">
|
||||
<el-option :label="TEXT.common.labels.all" value="ALL" />
|
||||
<el-option :label="TEXT.enums.distributionTargetType.ROLE" value="ROLE" />
|
||||
<el-option :label="TEXT.enums.distributionTargetType.USER" value="USER" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="item.target_type === 'ROLE'"
|
||||
v-model="item.target_id"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
class="target-select"
|
||||
>
|
||||
<el-option v-for="role in roleOptions" :key="role.value" :label="role.label" :value="role.value" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-else
|
||||
v-model="item.target_id"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
class="target-select"
|
||||
>
|
||||
<el-option v-for="member in memberOptions" :key="member.value" :label="member.label" :value="member.value" />
|
||||
</el-select>
|
||||
<el-button link type="danger" @click="removeTarget(index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" plain @click="addTarget" class="w-full dashed-button">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.actions.addTarget }}
|
||||
</el-button>
|
||||
<div class="target-row">
|
||||
<template v-if="distributeForm.target_type === 'ROLE'">
|
||||
<el-select
|
||||
v-model="distributeForm.role_ids"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
class="target-select"
|
||||
>
|
||||
<el-option v-for="role in roleOptions" :key="role.value" :label="role.label" :value="role.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else-if="distributeForm.target_type === 'USER'">
|
||||
<el-select
|
||||
v-model="distributeForm.user_ids"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="TEXT.common.placeholders.select"
|
||||
class="target-select"
|
||||
>
|
||||
<el-option v-for="member in memberOptions" :key="member.value" :label="member.label" :value="member.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<div v-else class="target-all">
|
||||
<el-tag effect="plain" type="info">{{ TEXT.common.labels.all }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.dueAt">
|
||||
<el-date-picker v-model="distributeForm.due_at" type="datetime" :placeholder="TEXT.common.placeholders.select" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="distributeVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
@@ -254,79 +215,40 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="submitVisible" :title="TEXT.modules.fileVersionManagement.messages.submitTitle" width="480px" destroy-on-close class="ctms-dialog">
|
||||
<el-form :model="submitForm" label-width="100px" class="ctms-form">
|
||||
<el-form-item :label="TEXT.modules.fileVersionManagement.fields.workflowTemplate">
|
||||
<el-select
|
||||
v-model="submitForm.template_id"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="TEXT.modules.fileVersionManagement.placeholders.workflowTemplate"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in workflowTemplates" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitVisible = false">{{ TEXT.common.actions.cancel }}</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="confirmSubmit">{{ TEXT.common.actions.confirm }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus";
|
||||
import { ArrowLeft, Upload, Plus, Download, Check, Close, Link, Share, Delete } from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from "element-plus";
|
||||
import { ArrowLeft, Upload, Download, Share } from "@element-plus/icons-vue";
|
||||
import {
|
||||
approveVersion,
|
||||
createAcknowledgement,
|
||||
createDistributions,
|
||||
deleteDocumentVersion,
|
||||
downloadDocumentVersion,
|
||||
fetchDocumentDetail,
|
||||
listDistributions,
|
||||
listWorkflowTemplates,
|
||||
makeVersionEffective,
|
||||
rejectVersion,
|
||||
submitVersion,
|
||||
uploadDocumentVersion,
|
||||
} from "../../api/documents";
|
||||
import { fetchAuditLogs } from "../../api/auditLogs";
|
||||
import { listMembers } from "../../api/members";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { fetchUsers } from "../../api/users";
|
||||
import type { Distribution, DocumentDetail, DocumentVersion, WorkflowTemplate } from "../../types/documents";
|
||||
import type { Distribution, DocumentDetail, DocumentVersion } from "../../types/documents";
|
||||
import type { Site, StudyMember } from "../../types/api";
|
||||
import type { UserInfo } from "../../types/api";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { TEXT } from "../../locales";
|
||||
import { displayEnum, displayText, getUserDisplayName } from "../../utils/display";
|
||||
|
||||
const getVersionStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case "APPROVED":
|
||||
return "success";
|
||||
case "DRAFT":
|
||||
return "info";
|
||||
case "SUBMITTED":
|
||||
return "warning";
|
||||
case "REJECTED":
|
||||
return "danger";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const documentId = computed(() => route.params.id as string);
|
||||
|
||||
const loading = ref(false);
|
||||
const versionLoading = ref(false);
|
||||
const distributionLoading = ref(false);
|
||||
const auditLoading = ref(false);
|
||||
const detail = reactive<DocumentDetail>({
|
||||
id: "",
|
||||
trial_id: "",
|
||||
@@ -344,11 +266,9 @@ const detail = reactive<DocumentDetail>({
|
||||
|
||||
const activeTab = ref("versions");
|
||||
const distributions = ref<Distribution[]>([]);
|
||||
const auditLogs = ref<any[]>([]);
|
||||
const users = ref<UserInfo[]>([]);
|
||||
const sites = ref<Site[]>([]);
|
||||
const members = ref<StudyMember[]>([]);
|
||||
const workflowTemplates = ref<WorkflowTemplate[]>([]);
|
||||
|
||||
const siteActiveMap = computed(() => {
|
||||
const map: Record<string, boolean> = {};
|
||||
@@ -358,29 +278,57 @@ const siteActiveMap = computed(() => {
|
||||
return map;
|
||||
});
|
||||
const isReadOnly = computed(() => !!detail.site_id && siteActiveMap.value[detail.site_id] === false);
|
||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||
|
||||
const userMap = computed(() =>
|
||||
users.value.reduce<Record<string, string>>((acc, user) => {
|
||||
acc[user.id] = getUserDisplayName(user) || user.email || user.username || user.id;
|
||||
const memberUserMap = computed(() =>
|
||||
members.value.reduce<Record<string, string>>((acc, member) => {
|
||||
const user = (member as any).user;
|
||||
const name = getUserDisplayName(user) || user?.email || user?.username || member.user_id;
|
||||
acc[member.user_id] = name;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const userMap = computed(() => {
|
||||
const map = { ...memberUserMap.value };
|
||||
users.value.forEach((user) => {
|
||||
map[user.id] = getUserDisplayName(user) || user.email || user.username || user.id;
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const displayCreator = (userId?: string | null) => {
|
||||
if (!userId) return TEXT.common.fallback;
|
||||
return userMap.value[userId] || userId;
|
||||
};
|
||||
|
||||
const displayParentVersion = (parentId?: string | null) => {
|
||||
if (!parentId) return TEXT.common.fallback;
|
||||
const hit = detail.version_timeline?.find((item: any) => item.id === parentId);
|
||||
return hit?.version_no ? `V${hit.version_no}` : TEXT.common.fallback;
|
||||
};
|
||||
|
||||
const displaySite = (siteId?: string | null) => {
|
||||
if (!siteId) return TEXT.enums.scopeType.GLOBAL;
|
||||
return sites.value.find((s) => s.id === siteId)?.name || siteId;
|
||||
};
|
||||
|
||||
const currentEffectiveVersion = computed(() => {
|
||||
if (!detail.current_effective_version_id) return null;
|
||||
return detail.version_timeline?.find((item: any) => item.id === detail.current_effective_version_id) || null;
|
||||
});
|
||||
|
||||
const distributionVersionId = computed(() => {
|
||||
if (detail.current_effective_version_id) return detail.current_effective_version_id;
|
||||
return detail.version_timeline?.[0]?.id || null;
|
||||
});
|
||||
|
||||
const uploadVisible = ref(false);
|
||||
const uploading = ref(false);
|
||||
const uploadFormRef = ref<FormInstance>();
|
||||
const uploadForm = reactive({
|
||||
version_no: "",
|
||||
version_date: "",
|
||||
change_summary: "",
|
||||
parent_version_id: "",
|
||||
});
|
||||
@@ -388,19 +336,12 @@ const uploadFile = ref<File | null>(null);
|
||||
|
||||
const distributeVisible = ref(false);
|
||||
const distributing = ref(false);
|
||||
const targetSeed = ref(0);
|
||||
const distributeForm = reactive({
|
||||
targets: [{ uid: 0, target_type: "ROLE", target_id: "" }],
|
||||
due_at: "",
|
||||
target_type: "ALL" as "ALL" | "ROLE" | "USER",
|
||||
role_ids: [] as string[],
|
||||
user_ids: [] as string[],
|
||||
});
|
||||
|
||||
const submitVisible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const submitForm = reactive({
|
||||
template_id: "",
|
||||
});
|
||||
const submitTarget = ref<DocumentVersion | null>(null);
|
||||
|
||||
const uploadRules: FormRules = {
|
||||
version_no: [
|
||||
{ required: true, message: TEXT.common.messages.required, trigger: "blur" },
|
||||
@@ -412,6 +353,7 @@ const uploadRules: FormRules = {
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
version_date: [{ required: true, message: TEXT.common.messages.required, trigger: "change" }],
|
||||
};
|
||||
|
||||
const roleOptions = computed(() => {
|
||||
@@ -447,12 +389,6 @@ const displayTarget = (row: Distribution) => {
|
||||
return row.target_id;
|
||||
};
|
||||
|
||||
const createTarget = () => ({
|
||||
uid: ++targetSeed.value,
|
||||
target_type: "ROLE",
|
||||
target_id: "",
|
||||
});
|
||||
|
||||
const canAction = (action: string) => {
|
||||
if (isReadOnly.value && action !== "view") return false;
|
||||
const list = detail.can_actions || [];
|
||||
@@ -487,6 +423,7 @@ const loadSites = async () => {
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (!isAdmin.value) return;
|
||||
try {
|
||||
const { data } = await fetchUsers({ limit: 500 });
|
||||
users.value = (data as any).items || data || [];
|
||||
@@ -521,13 +458,13 @@ const loadDetail = async () => {
|
||||
};
|
||||
|
||||
const loadDistributions = async () => {
|
||||
if (!detail.current_effective_version_id) {
|
||||
if (!distributionVersionId.value) {
|
||||
distributions.value = [];
|
||||
return;
|
||||
}
|
||||
distributionLoading.value = true;
|
||||
try {
|
||||
const { data } = await listDistributions(detail.current_effective_version_id);
|
||||
const { data } = await listDistributions(distributionVersionId.value);
|
||||
distributions.value = data || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
@@ -536,44 +473,15 @@ const loadDistributions = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadAuditLogs = async () => {
|
||||
if (!detail.trial_id) return;
|
||||
auditLoading.value = true;
|
||||
try {
|
||||
const { data } = await fetchAuditLogs(detail.trial_id, {
|
||||
entity_type: "DOCUMENT",
|
||||
entity_id: documentId.value,
|
||||
limit: 20,
|
||||
});
|
||||
auditLogs.value = Array.isArray(data) ? data : data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
} finally {
|
||||
auditLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadWorkflowTemplates = async () => {
|
||||
try {
|
||||
const params = detail.trial_id ? { trial_id: detail.trial_id } : undefined;
|
||||
const { data } = await listWorkflowTemplates(params);
|
||||
workflowTemplates.value = data || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = async (name: string) => {
|
||||
if (name === "distributions") {
|
||||
await loadDistributions();
|
||||
}
|
||||
if (name === "audit") {
|
||||
await loadAuditLogs();
|
||||
}
|
||||
};
|
||||
|
||||
const openUpload = () => {
|
||||
uploadForm.version_no = "";
|
||||
uploadForm.version_date = "";
|
||||
uploadForm.change_summary = "";
|
||||
uploadForm.parent_version_id = "";
|
||||
uploadFile.value = null;
|
||||
@@ -600,6 +508,9 @@ const submitUpload = async () => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("version_no", uploadForm.version_no);
|
||||
if (uploadForm.version_date) {
|
||||
formData.append("version_date", uploadForm.version_date);
|
||||
}
|
||||
if (uploadForm.change_summary) {
|
||||
formData.append("change_summary", uploadForm.change_summary);
|
||||
}
|
||||
@@ -619,73 +530,6 @@ const submitUpload = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const openSubmit = async (version: DocumentVersion) => {
|
||||
submitTarget.value = version;
|
||||
submitForm.template_id = "";
|
||||
submitVisible.value = true;
|
||||
if (!workflowTemplates.value.length) {
|
||||
await loadWorkflowTemplates();
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSubmit = async () => {
|
||||
if (!submitTarget.value) return;
|
||||
if (!submitForm.template_id) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.workflowTemplateRequired);
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await submitVersion(submitTarget.value.id, { template_id: submitForm.template_id, comment: "" });
|
||||
submitVisible.value = false;
|
||||
await loadDetail();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.submitSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const approve = async (version: DocumentVersion) => {
|
||||
versionLoading.value = true;
|
||||
try {
|
||||
await approveVersion(version.id, { comment: "" });
|
||||
await loadDetail();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.approveSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
versionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const reject = async (version: DocumentVersion) => {
|
||||
versionLoading.value = true;
|
||||
try {
|
||||
await rejectVersion(version.id, { comment: "" });
|
||||
await loadDetail();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.rejectSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
versionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const makeEffective = async (version: DocumentVersion) => {
|
||||
versionLoading.value = true;
|
||||
try {
|
||||
await makeVersionEffective(version.id);
|
||||
await loadDetail();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.effectiveSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
} finally {
|
||||
versionLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getFilename = (header?: string | null) => {
|
||||
if (!header) return null;
|
||||
const match = /filename\*=UTF-8''([^;]+)|filename="?([^\";]+)"?/i.exec(header);
|
||||
@@ -714,47 +558,72 @@ const downloadVersion = async (version: DocumentVersion) => {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteVersion = async (version: DocumentVersion) => {
|
||||
if (!version?.id) return;
|
||||
const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteDocumentVersion(version.id);
|
||||
ElMessage.success(TEXT.common.messages.deleteSuccess);
|
||||
await loadDetail();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const openDistribute = () => {
|
||||
distributeForm.targets = [createTarget()];
|
||||
distributeForm.due_at = "";
|
||||
distributeForm.target_type = "ALL";
|
||||
distributeForm.role_ids = [];
|
||||
distributeForm.user_ids = [];
|
||||
distributeVisible.value = true;
|
||||
if (!members.value.length) {
|
||||
loadMembers();
|
||||
}
|
||||
};
|
||||
|
||||
const addTarget = () => {
|
||||
distributeForm.targets.push(createTarget());
|
||||
};
|
||||
|
||||
const removeTarget = (index: number) => {
|
||||
distributeForm.targets.splice(index, 1);
|
||||
};
|
||||
|
||||
const onTargetTypeChange = (index: number) => {
|
||||
const target = distributeForm.targets[index];
|
||||
if (!target) return;
|
||||
target.target_id = "";
|
||||
const onTargetTypeChange = () => {
|
||||
distributeForm.role_ids = [];
|
||||
distributeForm.user_ids = [];
|
||||
};
|
||||
|
||||
const submitDistribute = async () => {
|
||||
if (!detail.current_effective_version_id) {
|
||||
if (!distributionVersionId.value) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.noEffective);
|
||||
return;
|
||||
}
|
||||
if (distributeForm.targets.some((t) => !t.target_id)) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.missingTarget);
|
||||
return;
|
||||
}
|
||||
distributing.value = true;
|
||||
try {
|
||||
const targets = distributeForm.targets.map((t) => ({
|
||||
target_type: t.target_type,
|
||||
target_id: t.target_id,
|
||||
}));
|
||||
await createDistributions(detail.current_effective_version_id, {
|
||||
let targets: { target_type: string; target_id: string }[] = [];
|
||||
if (distributeForm.target_type === "ALL") {
|
||||
targets = memberOptions.value.map((m) => ({
|
||||
target_type: "USER",
|
||||
target_id: m.value,
|
||||
}));
|
||||
} else if (distributeForm.target_type === "ROLE") {
|
||||
if (!distributeForm.role_ids.length) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.missingTarget);
|
||||
return;
|
||||
}
|
||||
targets = distributeForm.role_ids.map((role) => ({
|
||||
target_type: "ROLE",
|
||||
target_id: role,
|
||||
}));
|
||||
} else if (distributeForm.target_type === "USER") {
|
||||
if (!distributeForm.user_ids.length) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.missingTarget);
|
||||
return;
|
||||
}
|
||||
targets = distributeForm.user_ids.map((user) => ({
|
||||
target_type: "USER",
|
||||
target_id: user,
|
||||
}));
|
||||
}
|
||||
if (!targets.length) {
|
||||
ElMessage.warning(TEXT.modules.fileVersionManagement.messages.missingTarget);
|
||||
return;
|
||||
}
|
||||
await createDistributions(distributionVersionId.value, {
|
||||
targets,
|
||||
due_at: distributeForm.due_at || undefined,
|
||||
});
|
||||
distributeVisible.value = false;
|
||||
await loadDistributions();
|
||||
@@ -766,16 +635,6 @@ const submitDistribute = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const ack = async (distribution: Distribution, ackType: string) => {
|
||||
try {
|
||||
await createAcknowledgement(distribution.id, { ack_type: ackType });
|
||||
await loadDistributions();
|
||||
ElMessage.success(TEXT.modules.fileVersionManagement.messages.ackSuccess);
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => {
|
||||
if (detail.trial_id) {
|
||||
router.push(`/trial/${detail.trial_id}/documents`);
|
||||
@@ -789,6 +648,12 @@ const formatDate = (value?: string | null) => {
|
||||
return value.replace("T", " ").replace("Z", "").split(".")[0];
|
||||
};
|
||||
|
||||
const formatDateOnly = (value?: string | null) => {
|
||||
const formatted = formatDate(value);
|
||||
if (formatted === TEXT.common.fallback) return formatted;
|
||||
return formatted.split(" ")[0];
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDetail();
|
||||
await loadDistributions();
|
||||
@@ -846,6 +711,36 @@ watch(
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.target-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.target-type-preset {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.target-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.target-select {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.target-all {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,34 @@
|
||||
<template>
|
||||
<div class="ctms-page">
|
||||
<div class="ctms-page-header">
|
||||
<div>
|
||||
<h1 class="ctms-page-title">{{ TEXT.modules.fileVersionManagement.title }}</h1>
|
||||
</div>
|
||||
<div class="ctms-page-actions">
|
||||
<el-button type="primary" @click="openCreate">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.newDocument }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-card class="ctms-filter-card">
|
||||
<div class="ctms-filter-row">
|
||||
<div class="ctms-filter-item">
|
||||
<span class="ctms-filter-label">{{ TEXT.modules.fileVersionManagement.filters.scope }}</span>
|
||||
<el-select v-model="filters.scope_type" :placeholder="TEXT.modules.fileVersionManagement.filters.all" clearable>
|
||||
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="ctms-filter-item">
|
||||
<span class="ctms-filter-label">{{ TEXT.modules.fileVersionManagement.filters.site }}</span>
|
||||
<el-select v-model="filters.site_id" :placeholder="TEXT.modules.fileVersionManagement.filters.all" clearable filterable>
|
||||
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="ctms-filter-item">
|
||||
<span class="ctms-filter-label">{{ TEXT.modules.fileVersionManagement.filters.docType }}</span>
|
||||
<el-select v-model="filters.doc_type" :placeholder="TEXT.modules.fileVersionManagement.filters.all" clearable>
|
||||
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="ctms-filter-spacer"></div>
|
||||
<div class="ctms-filter-actions">
|
||||
<el-button type="primary" @click="load">{{ TEXT.common.actions.search }}</el-button>
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</div>
|
||||
<el-card shadow="never" class="main-content-card">
|
||||
<div class="filter-container">
|
||||
<el-form :inline="true" :model="filters" class="filter-form">
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-select v-model="filters.scope_type" :placeholder="TEXT.modules.fileVersionManagement.filters.scope" clearable class="filter-select-comp">
|
||||
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-select v-model="filters.site_id" :placeholder="TEXT.modules.fileVersionManagement.filters.site" clearable filterable class="filter-select-comp">
|
||||
<el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="" class="filter-item-form">
|
||||
<el-select v-model="filters.doc_type" :placeholder="TEXT.modules.fileVersionManagement.filters.docType" clearable class="filter-select-comp">
|
||||
<el-option v-for="item in docTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="filter-item-form">
|
||||
<el-button @click="resetFilters">{{ TEXT.common.actions.reset }}</el-button>
|
||||
</el-form-item>
|
||||
<div class="filter-spacer"></div>
|
||||
<el-button type="primary" @click="openCreate" class="header-action-btn">
|
||||
<el-icon class="el-icon--left"><Plus /></el-icon>
|
||||
{{ TEXT.modules.fileVersionManagement.newDocument }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="ctms-table-card">
|
||||
<el-table
|
||||
:data="sortedItems"
|
||||
v-loading="loading"
|
||||
@@ -86,7 +74,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="TEXT.modules.fileVersionManagement.columns.actions" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
|
||||
<el-button v-if="isAdmin" link type="danger" :disabled="isInactiveSite(row.site_id)" @click.stop="confirmDelete(row)">
|
||||
{{ TEXT.common.actions.delete }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -137,6 +125,7 @@ import { Plus } from "@element-plus/icons-vue";
|
||||
import { fetchDocuments, createDocument, deleteDocument } from "../../api/documents";
|
||||
import { fetchSites } from "../../api/sites";
|
||||
import { useStudyStore } from "../../store/study";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import type { DocumentSummary } from "../../types/documents";
|
||||
import type { Site } from "../../types/api";
|
||||
import { displayEnum, displayText } from "../../utils/display";
|
||||
@@ -145,6 +134,7 @@ import { TEXT } from "../../locales";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const study = useStudyStore();
|
||||
const auth = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const items = ref<DocumentSummary[]>([]);
|
||||
const sites = ref<Site[]>([]);
|
||||
@@ -178,11 +168,23 @@ const siteActiveMap = computed(() => {
|
||||
});
|
||||
return map;
|
||||
});
|
||||
const isAdmin = computed(() => {
|
||||
const role = auth.user?.role;
|
||||
return role === "ADMIN";
|
||||
});
|
||||
const isInactiveSite = (siteId?: string | null) => !!siteId && siteActiveMap.value[siteId] === false;
|
||||
const documentRowClass = ({ row }: { row: DocumentSummary }) =>
|
||||
`${row?.id ? "clickable-row" : ""}${isInactiveSite(row?.site_id) ? " row-inactive" : ""}`.trim();
|
||||
const filteredItems = computed(() =>
|
||||
items.value.filter((item) => {
|
||||
if (filters.scope_type && item?.scope_type !== filters.scope_type) return false;
|
||||
if (filters.site_id && item?.site_id !== filters.site_id) return false;
|
||||
if (filters.doc_type && item?.doc_type !== filters.doc_type) return false;
|
||||
return true;
|
||||
})
|
||||
);
|
||||
const sortedItems = computed(() =>
|
||||
[...items.value].sort((a, b) => Number(isInactiveSite(a?.site_id)) - Number(isInactiveSite(b?.site_id)))
|
||||
[...filteredItems.value].sort((a, b) => Number(isInactiveSite(a?.site_id)) - Number(isInactiveSite(b?.site_id)))
|
||||
);
|
||||
|
||||
const createForm = reactive({
|
||||
@@ -240,12 +242,7 @@ const load = async () => {
|
||||
if (!trialId.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await fetchDocuments({
|
||||
trial_id: trialId.value,
|
||||
scope_type: filters.scope_type || undefined,
|
||||
site_id: filters.site_id || undefined,
|
||||
doc_type: filters.doc_type || undefined,
|
||||
});
|
||||
const { data } = await fetchDocuments({ trial_id: trialId.value });
|
||||
items.value = data.items || [];
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
|
||||
@@ -258,7 +255,6 @@ const resetFilters = () => {
|
||||
filters.scope_type = "";
|
||||
filters.site_id = "";
|
||||
filters.doc_type = "";
|
||||
load();
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
@@ -343,24 +339,43 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-secondary {
|
||||
color: var(--ctms-text-secondary);
|
||||
.ctms-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
.main-content-card :deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
.filter-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--ctms-primary);
|
||||
.filter-form {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
.filter-item-form {
|
||||
margin-bottom: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.filter-select-comp {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.filter-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-action-btn {
|
||||
height: 36px;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user