Files
ctms/frontend/src/views/admin/SiteForm.vue
T
2026-02-27 09:06:06 +08:00

199 lines
6.3 KiB
Vue

<template>
<el-dialog append-to=".layout-main .content-wrapper" :title="site ? TEXT.modules.adminSites.editTitle : TEXT.modules.adminSites.newTitle" width="520px" v-model="visibleProxy" :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item :label="TEXT.common.fields.siteName" prop="name">
<el-input v-model="form.name" :placeholder="TEXT.common.placeholders.input + TEXT.common.fields.siteName" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.city" prop="city">
<el-input v-model="form.city" :placeholder="TEXT.modules.adminSites.cityPlaceholder" />
</el-form-item>
<el-form-item :label="TEXT.modules.adminSites.piLabel" prop="pi_name">
<el-input v-model="form.pi_name" :placeholder="TEXT.modules.adminSites.piPlaceholder" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.phone" prop="phone">
<el-input v-model="form.phone" :placeholder="TEXT.modules.adminSites.phonePlaceholder" />
</el-form-item>
<el-form-item :label="TEXT.common.fields.contact" prop="contact">
<el-select
v-model="form.craSelections"
multiple
filterable
:placeholder="TEXT.modules.adminSites.contactPlaceholder"
>
<el-option
v-for="user in craOptions"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleProxy = false">{{ TEXT.common.actions.cancel }}</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">{{ TEXT.common.actions.save }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { createSite, updateSite } from "../../api/sites";
import type { Site } from "../../types/api";
import { useAuthStore } from "../../store/auth";
import { evaluateAction } from "../../guards/actionGuard";
import { logAudit } from "../../audit";
import { TEXT, requiredMessage } from "../../locales";
const props = defineProps<{
visible: boolean;
studyId: string;
site?: Site | null;
members?: any[];
users?: any[];
}>();
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "saved"): void;
}>();
const visibleProxy = computed({
get: () => props.visible,
set: (val: boolean) => emit("update:visible", val),
});
const auth = useAuthStore();
const formRef = ref<FormInstance>();
const submitting = ref(false);
const form = reactive({
name: "",
city: "",
pi_name: "",
phone: "",
contact: "",
craSelections: [] as string[],
is_active: true,
});
const rules = reactive<FormRules>({
name: [{ required: true, message: requiredMessage(TEXT.common.fields.siteName), trigger: "blur" }],
});
const craOptions = computed(() => {
const userMap = (props.users || []).reduce<Record<string, string>>((acc, cur: any) => {
if (cur?.id) acc[cur.id] = cur.full_name || cur.username || cur.id;
return acc;
}, {});
return (props.members || []).map((m: any) => ({
label: userMap[m.user_id] || m.full_name || m.username || m.user_id,
value: m.user_id,
}));
});
const resetForm = () => {
form.name = "";
form.city = "";
form.pi_name = "";
form.phone = "";
form.contact = "";
form.craSelections = [];
form.is_active = true;
};
watch(
() => props.visible,
(val) => {
if (val) {
resetForm();
if (props.site) {
form.name = props.site.name;
form.city = props.site.city || "";
form.pi_name = props.site.pi_name || "";
form.phone = (props.site as any)?.contact_phone || (props.site as any)?.phone || "";
form.contact = props.site.contact || "";
const rawSelections = (props.site.contact || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const optionByLabel: Record<string, string> = {};
craOptions.value.forEach((opt) => {
optionByLabel[opt.label] = opt.value;
});
form.craSelections = rawSelections.map((s) => optionByLabel[s] || s);
form.is_active = props.site.is_active;
}
}
}
);
const onSubmit = async () => {
if (!formRef.value) return;
const decision = evaluateAction({
actorRole: auth.user?.role || null,
requiredPermission: "site.manage",
target: { siteId: props.site?.id, studyId: props.studyId },
});
if (!decision.allowed) {
ElMessage.warning(decision.reason || TEXT.common.messages.noPermission);
logAudit(decision.auditType, {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name,
reason: decision.reason,
severity: decision.severity,
});
return;
}
await formRef.value.validate();
submitting.value = true;
try {
const contact = form.craSelections.length ? form.craSelections.join(",") : form.contact;
if (props.site) {
await updateSite(props.studyId, props.site.id, {
name: form.name,
city: form.city,
pi_name: form.pi_name,
contact,
is_active: form.is_active,
});
ElMessage.success(TEXT.modules.adminSites.updateSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site.id,
targetName: props.site.name,
after: { name: form.name, is_active: form.is_active },
severity: "normal",
});
} else {
await createSite(props.studyId, {
name: form.name,
city: form.city,
pi_name: form.pi_name,
contact,
});
ElMessage.success(TEXT.modules.adminSites.createSuccess);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.studyId,
targetName: form.name,
after: { name: form.name },
severity: "normal",
});
}
emit("saved");
visibleProxy.value = false;
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || TEXT.common.messages.saveFailed);
logAudit("SITE_STATUS_CHANGED", {
targetId: props.site?.id || props.studyId,
targetName: props.site?.name || form.name,
after: { name: form.name },
severity: "warning",
reason: e?.response?.data?.message,
});
} finally {
submitting.value = false;
}
};
</script>