审计日志并入系统管理、规范项目成员获取逻辑
This commit is contained in:
@@ -1,33 +0,0 @@
|
|||||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
||||||
|
|
||||||
CREATE TABLE public.faq_replies (
|
|
||||||
id uuid NOT NULL,
|
|
||||||
faq_id uuid NOT NULL,
|
|
||||||
study_id uuid,
|
|
||||||
content text NOT NULL,
|
|
||||||
created_by uuid NOT NULL,
|
|
||||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
||||||
quote_reply_id uuid
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE public.faq_replies OWNER TO ctms_user;
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.faq_replies
|
|
||||||
ADD CONSTRAINT faq_replies_pkey PRIMARY KEY (id);
|
|
||||||
|
|
||||||
CREATE INDEX ix_faq_replies_faq_id ON public.faq_replies USING btree (faq_id);
|
|
||||||
CREATE INDEX ix_faq_replies_study_id ON public.faq_replies USING btree (study_id);
|
|
||||||
|
|
||||||
ALTER TABLE ONLY public.faq_replies
|
|
||||||
ADD CONSTRAINT faq_replies_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.users(id);
|
|
||||||
ALTER TABLE ONLY public.faq_replies
|
|
||||||
ADD CONSTRAINT faq_replies_faq_id_fkey FOREIGN KEY (faq_id) REFERENCES public.faq_items(id);
|
|
||||||
ALTER TABLE ONLY public.faq_replies
|
|
||||||
ADD CONSTRAINT faq_replies_quote_reply_id_fkey FOREIGN KEY (quote_reply_id) REFERENCES public.faq_replies(id);
|
|
||||||
ALTER TABLE ONLY public.faq_replies
|
|
||||||
ADD CONSTRAINT faq_replies_study_id_fkey FOREIGN KEY (study_id) REFERENCES public.studies(id);
|
|
||||||
|
|
||||||
INSERT INTO public.faq_replies (id, faq_id, study_id, content, created_by, created_at)
|
|
||||||
SELECT gen_random_uuid(), id, study_id, answer, created_by, updated_at
|
|
||||||
FROM public.faq_items
|
|
||||||
WHERE answer IS NOT NULL AND btrim(answer) <> '';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE public.faq_replies
|
|
||||||
ADD COLUMN IF NOT EXISTS is_deleted boolean DEFAULT false NOT NULL;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
ALTER TABLE public.faq_items
|
|
||||||
ADD COLUMN IF NOT EXISTS best_reply_id uuid;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'faq_items_best_reply_id_fkey'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE ONLY public.faq_items
|
|
||||||
ADD CONSTRAINT faq_items_best_reply_id_fkey
|
|
||||||
FOREIGN KEY (best_reply_id) REFERENCES public.faq_replies(id) ON DELETE SET NULL;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS ix_faq_items_best_reply_id ON public.faq_items USING btree (best_reply_id);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
ALTER TABLE public.faq_items
|
|
||||||
ADD COLUMN IF NOT EXISTS status character varying(20) DEFAULT 'PENDING' NOT NULL;
|
|
||||||
|
|
||||||
UPDATE public.faq_items SET status = 'PENDING' WHERE status IS NULL;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
ALTER TABLE public.faq_items
|
|
||||||
ADD COLUMN IF NOT EXISTS resolved_by_confirm boolean DEFAULT false NOT NULL;
|
|
||||||
|
|
||||||
UPDATE public.faq_items
|
|
||||||
SET status = CASE
|
|
||||||
WHEN best_reply_id IS NOT NULL THEN 'RESOLVED'
|
|
||||||
WHEN EXISTS (
|
|
||||||
SELECT 1 FROM public.faq_replies r
|
|
||||||
WHERE r.faq_id = faq_items.id AND r.is_deleted = false
|
|
||||||
) THEN 'PROCESSING'
|
|
||||||
ELSE 'PENDING'
|
|
||||||
END,
|
|
||||||
resolved_by_confirm = false
|
|
||||||
WHERE status = 'RESOLVED' AND best_reply_id IS NULL;
|
|
||||||
@@ -30,7 +30,7 @@ import { ElMessage } from "element-plus";
|
|||||||
import { fetchComments, createComment } from "../api/comments";
|
import { fetchComments, createComment } from "../api/comments";
|
||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
import { listMembers } from "../api/members";
|
import { listMembers } from "../api/members";
|
||||||
import { displayDateTime, displayUser } from "../utils/display";
|
import { displayDateTime, displayUser, getMemberDisplayName } from "../utils/display";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
studyId: string;
|
studyId: string;
|
||||||
@@ -72,8 +72,8 @@ const loadMembers = async () => {
|
|||||||
|
|
||||||
const memberMap = computed(() =>
|
const memberMap = computed(() =>
|
||||||
members.value.reduce<Record<string, string>>((acc, cur) => {
|
members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
|
const username = getMemberDisplayName(cur);
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
<el-icon><Suitcase /></el-icon>
|
<el-icon><Suitcase /></el-icon>
|
||||||
<span>项目管理</span>
|
<span>项目管理</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item v-if="isAdmin" index="/admin/audit-logs">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>审计日志</span>
|
||||||
|
</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="study.currentStudy">
|
<template v-if="study.currentStudy">
|
||||||
@@ -77,10 +81,6 @@
|
|||||||
<el-icon><Notebook /></el-icon>
|
<el-icon><Notebook /></el-icon>
|
||||||
<span>知识库</span>
|
<span>知识库</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item v-if="isAdmin || isPm" index="/study/audit-logs">
|
|
||||||
<el-icon><Document /></el-icon>
|
|
||||||
<span>审计日志</span>
|
|
||||||
</el-menu-item>
|
|
||||||
</template>
|
</template>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import { formatFileSize } from "./attachmentUtils";
|
|||||||
import { useAuthStore } from "../../store/auth";
|
import { useAuthStore } from "../../store/auth";
|
||||||
import { useStudyStore } from "../../store/study";
|
import { useStudyStore } from "../../store/study";
|
||||||
import { listMembers } from "../../api/members";
|
import { listMembers } from "../../api/members";
|
||||||
import { displayDateTime, displayUser } from "../../utils/display";
|
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../../utils/display";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
studyId: string;
|
studyId: string;
|
||||||
@@ -90,12 +90,12 @@ const loadMembers = async () => {
|
|||||||
|
|
||||||
const uploaderLabel = (row: any) => {
|
const uploaderLabel = (row: any) => {
|
||||||
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
|
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
|
const username = getMemberDisplayName(cur);
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
|
if (row?.uploaded_by && typeof row.uploaded_by === "object") {
|
||||||
return row.uploaded_by.display_name || row.uploaded_by.username || row.uploaded_by.id || "—";
|
return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || "—";
|
||||||
}
|
}
|
||||||
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
|
return displayUser(row.uploaded_by_id || row.uploaded_by, { members: memberMap });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -191,12 +191,6 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: Verification,
|
component: Verification,
|
||||||
meta: { title: "核查进度", requiresStudy: true },
|
meta: { title: "核查进度", requiresStudy: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "study/audit-logs",
|
|
||||||
name: "AuditLogs",
|
|
||||||
component: AuditLogs,
|
|
||||||
meta: { title: "审计日志", requiresStudy: true },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -234,6 +228,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: AdminSites,
|
component: AdminSites,
|
||||||
meta: { title: "中心管理", requiresAdmin: true },
|
meta: { title: "中心管理", requiresAdmin: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "audit-logs",
|
||||||
|
name: "AdminAuditLogs",
|
||||||
|
component: AuditLogs,
|
||||||
|
meta: { title: "审计日志", requiresAdmin: true },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,6 +261,9 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
auth.logout();
|
auth.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (token && !studyStore.currentStudy) {
|
||||||
|
await studyStore.ensureDefaultStudy();
|
||||||
|
}
|
||||||
if (!to.meta.public && !token) {
|
if (!to.meta.public && !token) {
|
||||||
next({ path: "/login" });
|
next({ path: "/login" });
|
||||||
return;
|
return;
|
||||||
@@ -287,6 +290,10 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
next({ path: "/workbench" });
|
next({ path: "/workbench" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (to.path === "/workbench" && studyStore.currentStudy) {
|
||||||
|
next({ path: "/study/home" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { fetchStudies } from "../api/studies";
|
||||||
import type { Study } from "../types/api";
|
import type { Study } from "../types/api";
|
||||||
|
|
||||||
const STUDY_KEY = "ctms_current_study";
|
const STUDY_KEY = "ctms_current_study";
|
||||||
@@ -32,6 +33,21 @@ export const useStudyStore = defineStore("study", () => {
|
|||||||
currentStudyRole.value = savedRole || null;
|
currentStudyRole.value = savedRole || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ensureDefaultStudy = async () => {
|
||||||
|
if (currentStudy.value) return currentStudy.value;
|
||||||
|
try {
|
||||||
|
const { data } = await fetchStudies();
|
||||||
|
const items = (data as any).items || [];
|
||||||
|
if (items.length) {
|
||||||
|
setCurrentStudy(items[0] as Study);
|
||||||
|
return items[0] as Study;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const clearCurrentStudy = () => {
|
const clearCurrentStudy = () => {
|
||||||
currentStudy.value = null;
|
currentStudy.value = null;
|
||||||
currentStudyRole.value = null;
|
currentStudyRole.value = null;
|
||||||
@@ -54,6 +70,7 @@ export const useStudyStore = defineStore("study", () => {
|
|||||||
setCurrentStudy,
|
setCurrentStudy,
|
||||||
setCurrentStudyRole,
|
setCurrentStudyRole,
|
||||||
loadCurrentStudy,
|
loadCurrentStudy,
|
||||||
|
ensureDefaultStudy,
|
||||||
clearCurrentStudy,
|
clearCurrentStudy,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,23 @@ export const displayEnum = (enumMap: Record<string, string>, value?: string | nu
|
|||||||
return enumMap[value] || displayFallback;
|
return enumMap[value] || displayFallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getUserDisplayName = (user?: any) => {
|
||||||
|
if (!user) return null;
|
||||||
|
return user.display_name || user.full_name || user.username || user.email || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMemberDisplayName = (member?: any) => {
|
||||||
|
if (!member) return null;
|
||||||
|
return (
|
||||||
|
getUserDisplayName(member.user) ||
|
||||||
|
member.display_name ||
|
||||||
|
member.full_name ||
|
||||||
|
member.username ||
|
||||||
|
member.email ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const displayUser = (
|
export const displayUser = (
|
||||||
userId?: string | null,
|
userId?: string | null,
|
||||||
opts?: { users?: Record<string, string>; members?: Record<string, string> }
|
opts?: { users?: Record<string, string>; members?: Record<string, string> }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import { useStudyStore } from "../store/study";
|
import { useStudyStore } from "../store/study";
|
||||||
|
|
||||||
const PERMISSIONS: Record<string, string[]> = {
|
const PERMISSIONS: Record<string, string[]> = {
|
||||||
"milestone.create": ["ADMIN", "PM"],
|
"milestone.create": ["ADMIN", "PM", "CRA", "PV", "IMP"],
|
||||||
"subject.create": ["ADMIN", "PM", "CRA"],
|
"subject.create": ["ADMIN", "PM", "CRA"],
|
||||||
"subject.enroll": ["ADMIN", "PM", "CRA"],
|
"subject.enroll": ["ADMIN", "PM", "CRA"],
|
||||||
"subject.complete": ["ADMIN", "PM", "CRA"],
|
"subject.complete": ["ADMIN", "PM", "CRA"],
|
||||||
@@ -26,7 +26,7 @@ const PERMISSIONS: Record<string, string[]> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const REASONS: Record<string, string> = {
|
const REASONS: Record<string, string> = {
|
||||||
"milestone.create": "仅项目负责人可以维护里程碑",
|
"milestone.create": "仅项目成员可维护里程碑",
|
||||||
"subject.enroll": "仅 PM/CRA 可更新受试者状态",
|
"subject.enroll": "仅 PM/CRA 可更新受试者状态",
|
||||||
"subject.complete": "仅 PM/CRA 可更新受试者状态",
|
"subject.complete": "仅 PM/CRA 可更新受试者状态",
|
||||||
"subject.drop": "仅 PM/CRA 可更新受试者状态",
|
"subject.drop": "仅 PM/CRA 可更新受试者状态",
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ import {
|
|||||||
setFaqStatus,
|
setFaqStatus,
|
||||||
} from "../api/faqs";
|
} from "../api/faqs";
|
||||||
import { listMembers } from "../api/members";
|
import { listMembers } from "../api/members";
|
||||||
import { displayDateTime, displayUser } from "../utils/display";
|
import { displayDateTime, displayUser, getMemberDisplayName, getUserDisplayName } from "../utils/display";
|
||||||
import { useAuthStore } from "../store/auth";
|
import { useAuthStore } from "../store/auth";
|
||||||
import { usePermission } from "../utils/permission";
|
import { usePermission } from "../utils/permission";
|
||||||
import FaqItemForm from "../components/FaqItemForm.vue";
|
import FaqItemForm from "../components/FaqItemForm.vue";
|
||||||
@@ -185,16 +185,28 @@ const bestReply = computed(() => {
|
|||||||
|
|
||||||
const memberMap = computed(() =>
|
const memberMap = computed(() =>
|
||||||
members.value.reduce<Record<string, string>>((acc, cur) => {
|
members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username = cur?.user?.display_name || cur?.user?.username || cur?.username;
|
const username = getMemberDisplayName(cur);
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|
||||||
const userMap = computed(() => {
|
const userMap = computed(() => {
|
||||||
if (!auth.user?.id) return {};
|
const map: Record<string, string> = {};
|
||||||
const name = auth.user.display_name || auth.user.username || auth.user.id;
|
if (auth.user?.id) {
|
||||||
return { [auth.user.id]: name };
|
const name = getUserDisplayName(auth.user) || auth.user.id;
|
||||||
|
map[auth.user.id] = name;
|
||||||
|
}
|
||||||
|
const push = (id?: string | null, name?: string | null) => {
|
||||||
|
if (!id || !name) return;
|
||||||
|
map[id] = name;
|
||||||
|
};
|
||||||
|
push(item.value?.created_by, item.value?.created_by_name || item.value?.created_by_display_name || item.value?.created_by_email);
|
||||||
|
replies.value.forEach((r) => {
|
||||||
|
push(r?.created_by, r?.created_by_name || r?.created_by_display_name || r?.created_by_email);
|
||||||
|
push(r?.quote?.created_by, r?.quote?.created_by_name || r?.quote?.created_by_display_name || r?.quote?.created_by_email);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
const categoryName = computed(() => {
|
const categoryName = computed(() => {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
getMilestoneStatusLabel,
|
getMilestoneStatusLabel,
|
||||||
getMilestoneTypeLabel,
|
getMilestoneTypeLabel,
|
||||||
} from "../dictionaries/milestone.dict";
|
} from "../dictionaries/milestone.dict";
|
||||||
import { displayDate, displayUser } from "../utils/display";
|
import { displayDate, displayUser, getMemberDisplayName, getUserDisplayName } from "../utils/display";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
@@ -134,17 +134,17 @@ const siteName = (row: any) => {
|
|||||||
|
|
||||||
const ownerName = (ownerId: string) => {
|
const ownerName = (ownerId: string) => {
|
||||||
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
|
const memberMap = members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username = cur?.user?.display_name || cur?.user?.full_name || cur?.user?.username || cur?.username;
|
const username = getMemberDisplayName(cur);
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
if (cur?.id) acc[cur.id] = username || cur.id;
|
if (cur?.id && username) acc[cur.id] = username;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const owner = milestone.value?.owner;
|
const owner = milestone.value?.owner;
|
||||||
const nameFromObject = owner?.display_name || owner?.full_name || owner?.username;
|
const nameFromObject = getUserDisplayName(owner);
|
||||||
if (nameFromObject) return nameFromObject;
|
if (nameFromObject) return nameFromObject;
|
||||||
|
|
||||||
return memberMap[ownerId] || ownerId || "—";
|
return memberMap[ownerId] || "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ import {
|
|||||||
getMilestoneStatusLabel,
|
getMilestoneStatusLabel,
|
||||||
getMilestoneTypeLabel,
|
getMilestoneTypeLabel,
|
||||||
} from "../dictionaries/milestone.dict";
|
} from "../dictionaries/milestone.dict";
|
||||||
import { displayDate, displayUser } from "../utils/display";
|
import { displayDate, displayUser, getMemberDisplayName } from "../utils/display";
|
||||||
|
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -150,14 +150,8 @@ const statusColor = (v: string) => getMilestoneStatusColor(v);
|
|||||||
|
|
||||||
const memberMap = computed(() =>
|
const memberMap = computed(() =>
|
||||||
members.value.reduce<Record<string, string>>((acc, cur) => {
|
members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username =
|
const username = getMemberDisplayName(cur);
|
||||||
cur?.user?.full_name ||
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
cur?.user?.display_name ||
|
|
||||||
cur?.user?.username ||
|
|
||||||
cur?.user?.email ||
|
|
||||||
cur?.username ||
|
|
||||||
cur?.email;
|
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ import { useAuthStore } from "../store/auth";
|
|||||||
import VerificationTable from "../components/VerificationTable.vue";
|
import VerificationTable from "../components/VerificationTable.vue";
|
||||||
import SiteSelect from "../components/selectors/SiteSelect.vue";
|
import SiteSelect from "../components/selectors/SiteSelect.vue";
|
||||||
import SubjectSelect from "../components/selectors/SubjectSelect.vue";
|
import SubjectSelect from "../components/selectors/SubjectSelect.vue";
|
||||||
import { displayDate } from "../utils/display";
|
import { displayDate, getMemberDisplayName } from "../utils/display";
|
||||||
|
|
||||||
const study = useStudyStore();
|
const study = useStudyStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
@@ -188,9 +188,9 @@ const subjectMap = computed(() =>
|
|||||||
|
|
||||||
const verifierMap = computed(() =>
|
const verifierMap = computed(() =>
|
||||||
members.value.reduce<Record<string, string>>((acc, cur) => {
|
members.value.reduce<Record<string, string>>((acc, cur) => {
|
||||||
const username = cur?.user?.display_name || cur?.user?.full_name || cur?.user?.username || cur?.username;
|
const username = getMemberDisplayName(cur);
|
||||||
if (cur?.user_id) acc[cur.user_id] = username || cur.user_id;
|
if (cur?.user_id && username) acc[cur.user_id] = username;
|
||||||
if (cur?.id) acc[cur.id] = username || cur.id;
|
if (cur?.id && username) acc[cur.id] = username;
|
||||||
return acc;
|
return acc;
|
||||||
}, {})
|
}, {})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,13 +151,12 @@ const eventTypeOptions = Object.entries(auditDict).map(([value, cfg]) => ({
|
|||||||
}));
|
}));
|
||||||
const userOptions = computed(() => users.value.map((u: any) => ({ label: u.username, value: u.id })));
|
const userOptions = computed(() => users.value.map((u: any) => ({ label: u.username, value: u.id })));
|
||||||
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
const isAdmin = computed(() => auth.user?.role === "ADMIN");
|
||||||
const canProjectExport = computed(() => !!study.currentStudy && (auth.user?.role === "ADMIN" || auth.user?.role === "PM" || study.currentStudyRole === "PM"));
|
const canProjectExport = computed(() => !!study.currentStudy && auth.user?.role === "ADMIN");
|
||||||
|
|
||||||
const ensureAccess = () => {
|
const ensureAccess = () => {
|
||||||
const role = auth.user?.role;
|
const role = auth.user?.role;
|
||||||
const studyRole = study.currentStudyRole;
|
if (role !== "ADMIN") {
|
||||||
if (!(role === "ADMIN" || role === "PM" || studyRole === "PM")) {
|
router.replace("/admin/users");
|
||||||
router.replace("/study/home");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user