199 lines
6.3 KiB
Vue
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>
|