diff --git a/database/init.sql b/database/init.sql index f5a336c4..4a40f748 100644 --- a/database/init.sql +++ b/database/init.sql @@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS public.studies ( protocol_no character varying(100), phase character varying(50), status character varying(20) NOT NULL DEFAULT 'DRAFT', + is_locked boolean NOT NULL DEFAULT false, visit_interval_days integer, visit_total integer, visit_window_start_offset integer, @@ -63,6 +64,7 @@ CREATE TABLE IF NOT EXISTS public.sites ( pi_name character varying(100), contact character varying(100), is_active boolean NOT NULL DEFAULT true, + enrollment_target integer, created_at timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT fk_sites_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT ); @@ -514,6 +516,7 @@ CREATE INDEX IF NOT EXISTS ix_drug_shipments_center_id ON public.drug_shipments CREATE INDEX IF NOT EXISTS ix_startup_feasibility_study_id ON public.startup_feasibility (study_id); CREATE INDEX IF NOT EXISTS ix_startup_ethics_study_id ON public.startup_ethics (study_id); CREATE INDEX IF NOT EXISTS ix_kickoff_meetings_study_id ON public.kickoff_meetings (study_id); +CREATE INDEX IF NOT EXISTS ix_kickoff_meetings_site_id ON public.kickoff_meetings (site_id); CREATE INDEX IF NOT EXISTS ix_training_authorizations_study_id ON public.training_authorizations (study_id); CREATE INDEX IF NOT EXISTS ix_knowledge_notes_study_id ON public.knowledge_notes (study_id); CREATE INDEX IF NOT EXISTS ix_faq_categories_study_id ON public.faq_categories (study_id); @@ -571,10 +574,11 @@ INSERT INTO public.studies ( created_by = EXCLUDED.created_by; INSERT INTO public.sites ( - id, study_id, name, city, pi_name, contact, is_active, created_at + id, study_id, name, city, pi_name, contact, enrollment_target, is_active, created_at ) VALUES - ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '北京', '李主任', '010-12345678', true, '2025-01-06 09:00:00+00'), - ('cccccccc-cccc-cccc-cccc-cccccccccccc', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '上海', '王主任', '021-12345678', true, '2025-01-06 09:10:00+00') + ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '北京', '李主任', '010-12345678', 80, true, '2025-01-06 09:00:00+00'), + ('cccccccc-cccc-cccc-cccc-cccccccccccc', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '上海', '王主任', '021-12345678', 60, true, '2025-01-06 09:10:00+00'), + ('dddddddd-cccc-bbbb-aaaa-eeeeeeeeeeee', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '示例中心 3', '北京', '赵主任', '010-12340000', 40, true, '2025-01-06 09:20:00+00') ON CONFLICT (id) DO NOTHING; INSERT INTO public.study_members ( @@ -659,16 +663,39 @@ SET site_id = (SELECT id FROM public.sites WHERE name ILIKE '%上海%' OR city I WHERE site_id IS NULL AND approval_no ILIKE '%-SH-%'; INSERT INTO public.kickoff_meetings ( - id, study_id, kickoff_date, attendees, created_by, created_at, updated_at -) VALUES ( - '3333aaaa-2222-3333-4444-555555555555', - (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), - '2025-01-12', - '["张三","李四","王五"]'::json, - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '2025-01-12 13:00:00+00', - '2025-01-12 13:00:00+00' -) ON CONFLICT (id) DO NOTHING; + id, study_id, site_id, kickoff_date, attendees, created_by, created_at, updated_at +) VALUES + ( + '3333aaaa-2222-3333-4444-555555555555', + (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + '2025-01-12', + '["张三","李四","王五"]'::json, + (SELECT id FROM public.users WHERE email = 'pm@example.com'), + '2025-01-12 13:00:00+00', + '2025-01-12 13:00:00+00' + ), + ( + '3333aaaa-2222-3333-4444-666666666666', + (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), + 'cccccccc-cccc-cccc-cccc-cccccccccccc', + '2025-01-14', + '["赵六","钱七","孙八"]'::json, + (SELECT id FROM public.users WHERE email = 'pm@example.com'), + '2025-01-14 13:00:00+00', + '2025-01-14 13:00:00+00' + ), + ( + '3333aaaa-2222-3333-4444-777777777777', + (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), + 'dddddddd-cccc-bbbb-aaaa-eeeeeeeeeeee', + '2025-01-16', + '["周九","吴十"]'::json, + (SELECT id FROM public.users WHERE email = 'pm@example.com'), + '2025-01-16 13:00:00+00', + '2025-01-16 13:00:00+00' + ) +ON CONFLICT (id) DO NOTHING; INSERT INTO public.training_authorizations ( id, study_id, name, role, site_name, trained, authorized, trained_date, authorized_date, remark, created_by, created_at, updated_at @@ -802,20 +829,6 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; -DO $$ -BEGIN - CREATE TYPE public.workflow_status AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'CANCELED'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; - -DO $$ -BEGIN - CREATE TYPE public.workflow_action_type AS ENUM ('SUBMIT', 'APPROVE', 'REJECT'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; - DO $$ BEGIN CREATE TYPE public.distribution_target_type AS ENUM ('SITE', 'ROLE', 'USER'); @@ -857,31 +870,6 @@ CREATE TABLE IF NOT EXISTS public.documents ( CONSTRAINT fk_documents_owner_id FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE SET NULL ); -CREATE TABLE IF NOT EXISTS public.workflow_templates ( - id uuid PRIMARY KEY, - trial_id uuid, - name character varying(200) NOT NULL, - description text, - is_active boolean NOT NULL DEFAULT true, - created_by uuid, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT fk_workflow_templates_trial_id FOREIGN KEY (trial_id) REFERENCES public.studies(id) ON DELETE RESTRICT, - CONSTRAINT fk_workflow_templates_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE SET NULL -); - -CREATE TABLE IF NOT EXISTS public.workflow_nodes ( - id uuid PRIMARY KEY, - template_id uuid NOT NULL, - node_order integer NOT NULL, - role character varying(50) NOT NULL, - name character varying(100), - is_required boolean NOT NULL DEFAULT true, - created_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT uq_workflow_nodes_order UNIQUE (template_id, node_order), - CONSTRAINT fk_workflow_nodes_template_id FOREIGN KEY (template_id) REFERENCES public.workflow_templates(id) ON DELETE CASCADE -); - CREATE TABLE IF NOT EXISTS public.document_versions ( id uuid PRIMARY KEY, document_id uuid NOT NULL, @@ -919,37 +907,6 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; -CREATE TABLE IF NOT EXISTS public.version_workflows ( - id uuid PRIMARY KEY, - document_id uuid NOT NULL, - version_id uuid NOT NULL, - template_id uuid NOT NULL, - status public.workflow_status NOT NULL DEFAULT 'PENDING', - current_node integer, - submitted_by uuid, - submitted_at timestamp with time zone, - completed_at timestamp with time zone, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT uq_version_workflow_version UNIQUE (version_id), - CONSTRAINT fk_version_workflows_document_id FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE CASCADE, - CONSTRAINT fk_version_workflows_version_id FOREIGN KEY (version_id) REFERENCES public.document_versions(id) ON DELETE CASCADE, - CONSTRAINT fk_version_workflows_template_id FOREIGN KEY (template_id) REFERENCES public.workflow_templates(id) ON DELETE RESTRICT, - CONSTRAINT fk_version_workflows_submitted_by FOREIGN KEY (submitted_by) REFERENCES public.users(id) ON DELETE SET NULL -); - -CREATE TABLE IF NOT EXISTS public.workflow_actions ( - id uuid PRIMARY KEY, - workflow_id uuid NOT NULL, - node_order integer, - action public.workflow_action_type NOT NULL, - actor_id uuid, - comment text, - acted_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT fk_workflow_actions_workflow_id FOREIGN KEY (workflow_id) REFERENCES public.version_workflows(id) ON DELETE CASCADE, - CONSTRAINT fk_workflow_actions_actor_id FOREIGN KEY (actor_id) REFERENCES public.users(id) ON DELETE SET NULL -); - CREATE TABLE IF NOT EXISTS public.distributions ( id uuid PRIMARY KEY, document_id uuid NOT NULL, @@ -988,93 +945,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_document_versions_doc_no ON public.document CREATE INDEX IF NOT EXISTS ix_document_versions_status ON public.document_versions (document_id, status); CREATE UNIQUE INDEX IF NOT EXISTS uq_document_versions_pending ON public.document_versions (document_id) WHERE status IN ('SUBMITTED', 'APPROVED'); -CREATE UNIQUE INDEX IF NOT EXISTS uq_document_pending_workflow ON public.version_workflows (document_id) - WHERE status IN ('PENDING', 'APPROVED'); CREATE INDEX IF NOT EXISTS ix_distributions_version_target ON public.distributions (version_id, target_type, target_id); CREATE INDEX IF NOT EXISTS ix_acknowledgements_distribution_type ON public.acknowledgements (distribution_id, ack_type); CREATE INDEX IF NOT EXISTS ix_audit_logs_entity_at ON public.audit_logs (entity_type, entity_id, created_at); +DO $$ +BEGIN + ALTER TABLE public.kickoff_meetings + ADD CONSTRAINT uq_kickoff_meetings_study_site UNIQUE (study_id, site_id); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + -- ========================================= -- Demo data for document version management -- ========================================= -INSERT INTO public.workflow_templates ( - id, trial_id, name, description, is_active, created_by, created_at, updated_at -) VALUES - ( - '11111111-aaaa-bbbb-cccc-111111111111', - (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), - '标准审批(PM 单签)', - 'PM 单节点审批,适用于一般文件', - true, - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '2025-01-07 08:00:00+00', - '2025-01-07 08:00:00+00' - ), - ( - '11111111-aaaa-bbbb-cccc-111111111112', - (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), - '严格审批(CRA→PM→QA)', - '三节点审批,适用于关键文件', - true, - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '2025-01-07 08:20:00+00', - '2025-01-07 08:20:00+00' - ) -ON CONFLICT (id) DO UPDATE -SET name = EXCLUDED.name, - description = EXCLUDED.description, - is_active = EXCLUDED.is_active, - trial_id = EXCLUDED.trial_id, - created_by = EXCLUDED.created_by, - updated_at = EXCLUDED.updated_at; - -INSERT INTO public.workflow_nodes ( - id, template_id, node_order, role, name, is_required, created_at -) VALUES - ( - '22222222-aaaa-bbbb-cccc-222222222222', - '11111111-aaaa-bbbb-cccc-111111111111', - 1, - 'PM', - '项目经理审批', - true, - '2025-01-07 08:10:00+00' - ), - ( - '22222222-aaaa-bbbb-cccc-222222222223', - '11111111-aaaa-bbbb-cccc-111111111112', - 1, - 'CRA', - 'CRA 初审', - true, - '2025-01-07 08:30:00+00' - ), - ( - '22222222-aaaa-bbbb-cccc-222222222224', - '11111111-aaaa-bbbb-cccc-111111111112', - 2, - 'PM', - '项目经理复审', - true, - '2025-01-07 08:40:00+00' - ), - ( - '22222222-aaaa-bbbb-cccc-222222222225', - '11111111-aaaa-bbbb-cccc-111111111112', - 3, - 'QA', - 'QA 终审', - true, - '2025-01-07 08:50:00+00' - ) -ON CONFLICT (id) DO UPDATE -SET template_id = EXCLUDED.template_id, - node_order = EXCLUDED.node_order, - role = EXCLUDED.role, - name = EXCLUDED.name, - is_required = EXCLUDED.is_required; - INSERT INTO public.documents ( id, trial_id, site_id, doc_no, doc_type, title, scope_type, owner_id, status, current_effective_version_id, description, created_at, updated_at ) VALUES @@ -1223,47 +1109,6 @@ SET current_effective_version_id = '88888888-aaaa-bbbb-cccc-888888888888', WHERE id = '55555555-aaaa-bbbb-cccc-555555555555' AND current_effective_version_id IS NULL; -INSERT INTO public.version_workflows ( - id, document_id, version_id, template_id, status, current_node, submitted_by, submitted_at, completed_at, created_at, updated_at -) VALUES - ( - '88888888-aaaa-bbbb-cccc-888888888888', - '33333333-aaaa-bbbb-cccc-333333333333', - '66666666-aaaa-bbbb-cccc-666666666666', - '11111111-aaaa-bbbb-cccc-111111111111', - 'APPROVED', - 1, - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '2025-01-12 10:00:00+00', - '2025-01-12 11:00:00+00', - '2025-01-12 10:00:00+00', - '2025-01-12 11:00:00+00' - ) -ON CONFLICT (id) DO NOTHING; - -INSERT INTO public.workflow_actions ( - id, workflow_id, node_order, action, actor_id, comment, acted_at -) VALUES - ( - '99999999-aaaa-bbbb-cccc-999999999999', - '88888888-aaaa-bbbb-cccc-888888888888', - 1, - 'SUBMIT', - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '提交审批', - '2025-01-12 10:00:00+00' - ), - ( - 'aaaa0000-bbbb-cccc-dddd-111111111111', - '88888888-aaaa-bbbb-cccc-888888888888', - 1, - 'APPROVE', - (SELECT id FROM public.users WHERE email = 'pm@example.com'), - '批准通过', - '2025-01-12 11:00:00+00' - ) -ON CONFLICT (id) DO NOTHING; - INSERT INTO public.distributions ( id, document_id, version_id, target_type, target_id, status, due_at, closed_at, created_by, created_at ) VALUES diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index ef389b07..28bd3488 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -476,16 +476,33 @@ router.beforeEach(async (to, _from, next) => { } } const token = getToken(); + const isAdmin = auth.user?.role === "ADMIN"; if (token && !studyStore.currentStudy) { - await studyStore.ensureDefaultStudy(); + if (isAdmin) { + await studyStore.ensureDefaultActiveStudy(); + } else { + await studyStore.ensureDefaultStudy(); + } } if (!to.meta.public && !token) { next({ path: "/login" }); return; } + if (isAdmin && (to.path === "/" || to.path === "/workbench")) { + next({ path: studyStore.currentStudy ? "/project/overview" : "/admin/users" }); + return; + } if ((to.path === "/login" || to.path === "/register") && token) { if (!auth.forceLogin) { - next({ path: studyStore.currentStudy ? "/project/overview" : "/workbench" }); + next({ + path: isAdmin + ? studyStore.currentStudy + ? "/project/overview" + : "/admin/users" + : studyStore.currentStudy + ? "/project/overview" + : "/workbench", + }); return; } } @@ -502,7 +519,7 @@ router.beforeEach(async (to, _from, next) => { } } if (to.meta.requiresStudy && !studyStore.currentStudy) { - next({ path: "/workbench" }); + next({ path: isAdmin ? "/admin/users" : "/workbench" }); return; } next(); diff --git a/frontend/src/store/study.ts b/frontend/src/store/study.ts index 640b0055..cdc4baf2 100644 --- a/frontend/src/store/study.ts +++ b/frontend/src/store/study.ts @@ -63,6 +63,22 @@ export const useStudyStore = defineStore("study", () => { return null; }; + const ensureDefaultActiveStudy = async () => { + if (currentStudy.value) return currentStudy.value; + try { + const { data } = await fetchStudies(); + const items = (data as any).items || []; + const active = items.find((study: Study) => study.status === "ACTIVE" && !study.is_locked); + if (active) { + setCurrentStudy(active as Study); + return active as Study; + } + } catch { + return null; + } + return null; + }; + const clearCurrentStudy = () => { currentStudy.value = null; currentStudyRole.value = null; @@ -106,6 +122,7 @@ export const useStudyStore = defineStore("study", () => { setViewContext, loadCurrentStudy, ensureDefaultStudy, + ensureDefaultActiveStudy, clearCurrentStudy, }; }); diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index a117f24a..d6915169 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -115,6 +115,7 @@ import { reactive, ref, onMounted, onUnmounted } from "vue"; import { useRouter } from "vue-router"; import { ElMessage, type FormInstance, type FormRules } from "element-plus"; import { useAuthStore } from "../store/auth"; +import { useStudyStore } from "../store/study"; import { getCachedCredential, setCachedCredential, clearCachedCredential } from "../utils/auth"; import { TEXT, requiredMessage } from "../locales"; @@ -215,7 +216,17 @@ const onSubmit = async () => { } else { clearCachedCredential(); } - router.push("/"); + if (auth.user?.role === "ADMIN") { + const studyStore = useStudyStore(); + await studyStore.ensureDefaultActiveStudy(); + if (studyStore.currentStudy) { + router.push("/project/overview"); + } else { + router.push("/admin/users"); + } + } else { + router.push("/"); + } } catch (error: any) { const detail = error?.response?.data?.detail || error?.response?.data?.message; ElMessage.error(detail || TEXT.modules.auth.authFailed);