This commit is contained in:
brusnitsyn
2026-02-20 17:28:16 +09:00
parent 94e374c32b
commit 52a80ccd3b
41 changed files with 2555 additions and 206 deletions

View File

@@ -0,0 +1,13 @@
<script setup>
</script>
<template>
<div class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<slot />
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,65 @@
<script setup>
import { NFormItem, NCard, NScrollbar } from 'naive-ui'
import {computed, ref, watch} from "vue";
const props = defineProps({
header: {
type: String,
default: ''
},
feedback: {
type: String,
default: ''
},
minH: {
type: [String, Number],
default: null
},
maxH: {
type: [String, Number],
default: null
},
})
const hasHeader = computed(() => props.header.trim().length > 0)
const hasFeedback = computed(() => props.feedback.trim().length > 0)
const hasMinH = computed(() => props.minH.trim().length > 0 || Number.isInteger(props.minH))
const hasMaxH = computed(() => props.maxH.trim().length > 0 || Number.isInteger(props.maxH))
const styles = ref([])
watch(() => [props.minH, props.maxH], ([minH, maxH]) => {
const sizeStyles = []
if (minH === null) return
if (minH.trim().length > 0) {
sizeStyles.push(`min-height: ${minH};`)
} else if (Number.isInteger(minH)) {
sizeStyles.push(`min-height: ${minH}px;`)
}
if (maxH === null) return
if (maxH.trim().length > 0) {
sizeStyles.push(`max-height: ${maxH};`)
} else if (Number.isInteger(maxH)) {
sizeStyles.push(`max-height: ${maxH}px;`)
}
styles.value = styles.value.concat(sizeStyles)
}, {
immediate: true
})
</script>
<template>
<NFormItem :show-label="hasHeader" :label="header" :show-feedback="hasFeedback" :feedback="feedback">
<NCard>
<NScrollbar :style="styles">
<slot />
</NScrollbar>
</NCard>
</NFormItem>
</template>
<style scoped>
</style>

View File

@@ -6,7 +6,7 @@ import {router, useForm} from "@inertiajs/vue3";
const authStore = useAuthStore()
const userOptions = computed(() => {
return authStore.availableRoles.map(itm => {
return authStore.availableRoles?.map(itm => {
return {
label: itm.name,
value: itm.role_id
@@ -15,7 +15,7 @@ const userOptions = computed(() => {
})
const formRole = useForm({
role_id: authStore.user.role.role_id
role_id: authStore.user.role?.role_id
})
const onChangeRole = (roleId) => {
formRole.post('/user/role/change', {

View File

@@ -0,0 +1,47 @@
<script setup>
import {NH1, NFlex, NSpace, NP} from 'naive-ui'
import {TbUsers} from "vue-icons-plus/tb";
import AppLayout from "../../Layouts/AppLayout.vue";
import {Link} from "@inertiajs/vue3";
import {computed} from "vue";
import {format} from "date-fns";
import {useNow} from "@vueuse/core";
import {ru} from "date-fns/locale";
import StartButton from "../../Components/StartButton.vue";
const currentDate = computed(() => {
const formatted = format(useNow().value, 'PPPPpp', {
locale: ru
})
return formatted.charAt(0).toUpperCase() + formatted.slice(1)
})
</script>
<template>
<AppLayout>
<div class="flex flex-col justify-start items-center mt-12">
<NFlex vertical align="center" justify="center" class="max-w-xl w-full">
<NSpace vertical align="center">
<NH1 class="mb-0!">
Панель администратора
</NH1>
<NP class="mb-4!">
{{ currentDate }}
</NP>
</NSpace>
<StartButton title="Учетные записи"
description="Создание и редактирование учетных записей"
href="/admin/users"
:tag="Link"
:icon="TbUsers"
/>
</NFlex>
</div>
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,61 @@
<script setup>
import {NButton, NFlex, NGi, NGrid, NH2, NForm, NFormItem, NInput} from "naive-ui";
import AppLayout from "../../../Layouts/AppLayout.vue";
import AppContainer from "../../../Components/AppContainer.vue";
import AppPanel from "../../../Components/AppPanel.vue";
import {useForm} from "@inertiajs/vue3";
import {ref} from "vue";
const props = defineProps({
departments: {
type: Array,
default: []
},
roles: {
type: Array,
default: []
}
})
const form = ref({
'name': '',
'login': '',
'password': '',
'is_active': true,
})
</script>
<template>
<AppLayout>
<template #header>
<NFlex align="center" justify="space-between" class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<NH2>
Создание нового пользователя
</NH2>
</NFlex>
</template>
<AppContainer>
<AppPanel>
<NForm v-model:model="form">
<NFormItem label="Имя">
<NInput v-model:value="form.name" />
</NFormItem>
<NFormItem label="Логин">
<NInput v-model:value="form.login" />
</NFormItem>
<NFormItem label="Логин">
<NInput v-model:value="form.password" />
</NFormItem>
<NFormItem label="Логин">
<NInput v-model:value="form.password" />
</NFormItem>
</NForm>
</AppPanel>
</AppContainer>
</AppLayout>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,81 @@
<script setup>
import AppLayout from "../../../Layouts/AppLayout.vue";
import {NFlex, NH2, NSpace, NButton, NDataTable} from 'naive-ui'
import {h, ref} from "vue";
import {Link} from "@inertiajs/vue3";
const props = defineProps({
users: {
type: Array,
default: []
}
})
const columns = ref([
{
title: 'Имя',
key: 'name'
},
{
title: 'Логин',
key: 'login'
},
{
title: 'Активен',
key: 'is_active',
render: (row) => {
return row.is_active ? 'Да' : 'Нет'
}
},
{
title: 'Дата создания',
key: 'created_at'
},
{
title: 'Дата изменения',
key: 'updated_at'
},
{
title: 'Действия',
key: 'actions',
render: (row) => {
return h(NButton, {
text: true,
size: 'small',
tag: Link,
href: `/admin/users/${row.id}`
}, 'Редактировать')
}
},
])
</script>
<template>
<AppLayout>
<template #header>
<NFlex align="center" justify="space-between" class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<NH2>
Учетные записи
</NH2>
<NSpace>
<NButton :tag="Link" href="/admin/users/new" type="primary">
Создать учетную запись
</NButton>
</NSpace>
</NFlex>
</template>
<NDataTable class="max-w-6xl mx-auto mb-4 w-full" :columns="columns" :data="users" />
</AppLayout>
</template>
<style scoped>
:deep(.n-h) {
margin: 0;
}
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
</style>

View File

@@ -0,0 +1,120 @@
<script setup>
import {NButton, NDataTable, NScrollbar, NList, NListItem, NGrid, NGi, NFlex, NH2, NSpace} from "naive-ui";
import AppLayout from "../../../Layouts/AppLayout.vue";
import AppPanel from "../../../Components/AppPanel.vue";
import AppContainer from "../../../Components/AppContainer.vue";
const props = defineProps({
userData: {
type: Object,
default: {}
},
roles: {
type: Array,
default: []
},
departments: {
type: Array,
default: []
}
})
</script>
<template>
<AppLayout>
<template #header>
<NFlex align="center" justify="space-between" class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<NH2>
{{ userData.name }}
</NH2>
<NSpace>
<NButton type="primary">
Создать учетную запись
</NButton>
</NSpace>
</NFlex>
</template>
<AppContainer>
<NGrid cols="2" x-gap="12" y-gap="12">
<NGi>
<AppPanel header="Информация о пользователе" min-h="148px" max-h="148px">
<NSpace vertical>
<NFlex align="center" justify="space-between">
<div>
Имя
</div>
<div>
{{ userData.name }}
</div>
</NFlex>
<NFlex align="center" justify="space-between">
<div>
Логин
</div>
<div>
{{ userData.login }}
</div>
</NFlex>
<NFlex align="center" justify="space-between">
<div>
Активен
</div>
<div>
{{ userData.is_active ? 'Да' : 'Нет' }}
</div>
</NFlex>
<NFlex align="center" justify="space-between">
<div>
Дата создания
</div>
<div>
{{ userData.created_at }}
</div>
</NFlex>
<NFlex align="center" justify="space-between">
<div>
Дата обновления
</div>
<div>
{{ userData.updated_at }}
</div>
</NFlex>
</NSpace>
</AppPanel>
</NGi>
<NGi>
</NGi>
<NGi>
<AppPanel :header="`Доступные отделения (${departments.length})`" min-h="148px" max-h="148px">
<NList>
<NListItem v-for="department in departments">
{{ department.name_full }}
</NListItem>
</NList>
</AppPanel>
</NGi>
<NGi>
<AppPanel :header="`Доступные роли (${roles.length})`" min-h="148px" max-h="148px">
<NList>
<NListItem v-for="role in roles">
{{ role.name }}
</NListItem>
</NList>
</AppPanel>
</NGi>
</NGrid>
</AppContainer>
</AppLayout>
</template>
<style scoped>
:deep(.n-h) {
margin: 0;
}
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
</style>

View File

@@ -7,7 +7,7 @@ import {computed, ref} from "vue";
import {format} from "date-fns";
import {ru} from "date-fns/locale";
import {useNow} from "@vueuse/core";
import {TbArticle, TbChartTreemap, TbDoorExit} from "vue-icons-plus/tb";
import {TbArticle, TbChartTreemap, TbDoorExit, TbUserCog} from "vue-icons-plus/tb";
import {useReportStore} from "../Stores/report.js";
import SelectUserModal from "./Report/Components/SelectUserModal.vue";
import {Link} from "@inertiajs/vue3";
@@ -58,6 +58,12 @@ const reportButtonType = computed(() => authStore.isDoctor ? 'button' : Link)
:href="`/statistic`"
:icon="TbChartTreemap"
/>
<StartButton v-if="false" title="Панель администратора"
description="Управление приложением"
href="/admin"
:tag="Link"
:icon="TbUserCog"
/>
<StartButton title="Выйти из системы"
description="Завершение работы с текущей учетной записью"
href="/dashboard"

View File

@@ -18,9 +18,11 @@ const authStore = useAuthStore()
const reportStore = useReportStore()
const onSubmit = () => {
reportStore.sendReportForm({
departmentId: authStore.userDepartment.department_id
reportStore.reportFormRef?.validate((errors) => {
if (!errors) reportStore.sendReportForm()
else window.$message.error('Ошибка отправки отчета')
})
// reportStore.sendReportForm()
}

View File

@@ -2,16 +2,42 @@
import {NCard, NSkeleton, NSpace, NFlex, NFormItem, NForm, NInputNumber, NStatistic} from "naive-ui";
import {useReportStore} from "../../../Stores/report.js";
import {useAuthStore} from "../../../Stores/auth.js";
import {computed, onMounted, ref, watch} from "vue";
const reportStore = useReportStore()
const authStore = useAuthStore()
const formRef = ref()
const rules = computed(() => {
const rawRules = {}
for (const metrika of reportStore.reportInfo.metrikaItems) {
const rule = {}
rule.required = metrika.is_required
rule.trigger = ['input', 'blur']
if (metrika.data_type === 'integer') {
rule.validator = (rule, value) => {
if (!/^\d*$/.test(value)) {
return new Error()
}
return true
}
}
rawRules[`metrika_item_${metrika.metrika_item_id}`] = rule
}
return rawRules
})
watch(() => formRef.value, (nv) => {
reportStore.reportFormRef = nv
})
</script>
<template>
<div class="grid grid-cols-[1fr_auto] gap-x-3">
<NCard v-if="reportStore.reportInfo?.report?.isActiveSendButton">
<NForm>
<NCard>
<NForm ref="formRef" :model="reportStore.reportForm" :disabled="!reportStore.reportInfo?.report?.isActiveSendButton" :rules="rules">
<template v-if="reportStore.isLoadReportInfo">
<NFlex>
<NSkeleton class="rounded-md w-[246px]! h-[65px]!" />
@@ -22,9 +48,9 @@ const authStore = useAuthStore()
<NFlex v-else justify="space-between" align="center">
<NSpace>
<template v-for="metrikaItem in reportStore.reportInfo?.metrikaItems">
<NFormItem :label="metrikaItem.name" :show-feedback="false">
<NFormItem :label="metrikaItem.name" :show-feedback="false" :required="metrikaItem.is_required" :path="`metrika_item_${metrikaItem.metrika_item_id}`">
<NInputNumber v-model:value="reportStore.reportForm[`metrika_item_${metrikaItem.metrika_item_id}`]"
:default-value="metrikaItem.default_value" />
:default-value="metrikaItem.default_value" min="0" />
</NFormItem>
</template>
</NSpace>
@@ -39,6 +65,19 @@ const authStore = useAuthStore()
<NStatistic label="Умерло" :value="reportStore.reportInfo?.department?.deadCount" />
</div>
</NCard>
<NCard class="min-w-[120px] max-w-[120px] min-h-[100px] max-h-[102px] h-full"
style="--n-padding-top: 0; --n-padding-bottom: 0; --n-padding-left: 8px; --n-padding-right: 8px;">
<div class="w-full h-full flex flex items-center justify-center">
<NStatistic :value="reportStore.reportInfo.department.percentDead">
<template #label>
<div class="flex flex-col">
<span>Летальность</span>
<span>%</span>
</div>
</template>
</NStatistic>
</div>
</NCard>
<NCard class="min-w-[120px] max-w-[120px] min-h-[100px] max-h-[102px] h-full"
style="--n-padding-top: 0; --n-padding-bottom: 0; --n-padding-left: 8px; --n-padding-right: 8px;">
<div class="w-full h-full flex flex items-center justify-center">

View File

@@ -58,11 +58,11 @@ const currentDate = computed(() => {
<div class="grid grid-cols-[auto_1fr_auto] items-center">
<NSpace align="center">
<NTag v-if="isFillableMode" type="info" :bordered="false">
{{ authStore.userDepartment.name_full }}
{{ reportStore.reportInfo.department.department_name }}
</NTag>
<DepartmentSelect v-if="isReadonlyMode" />
<NTag v-if="reportStore.reportInfo?.report?.userName" type="warning">
Ответственный: {{ reportStore.reportInfo?.report.userName }}
<NTag v-if="reportStore.reportInfo.report.userName" type="warning">
Ответственный: {{ reportStore.reportInfo.report.userName }}
</NTag>
</NSpace>

View File

@@ -23,7 +23,8 @@ const fetchPatientCount = async () => {
const data = {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1]
endAt: reportStore.timestampCurrentRange[1],
departmentId: reportStore.reportInfo.department.department_id
}
await axios.post('/api/mis/patients/count', data).then((res) => {
countPatient.value = res.data

View File

@@ -229,6 +229,7 @@ const fetchPatients = async () => {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1],
departmentId: reportStore.reportInfo.department.department_id
}
await axios.post('/api/mis/patients', data).then((res) => {
patientsData.value[props.status] = reportStore.addRowNumbers(res.data)

View File

@@ -1,56 +1,139 @@
<script setup>
import { NSelect, NModal, NForm, NFormItem, NButton } from 'naive-ui'
import { NSelect, NModal, NForm, NFormItem, NButton, NAlert } from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import {computed, onMounted, ref} from "vue";
import {router} from "@inertiajs/vue3";
import {computed, onMounted, ref, watch} from "vue";
import {router, Link} from "@inertiajs/vue3";
import {useDebounceFn} from "@vueuse/core";
const show = defineModel('show')
const reportStore = useReportStore()
const formRef = ref()
const users = computed(() => reportStore.departmentUsers.map(itm => ({
const rawUsers = ref([])
const rawDepartments = ref([])
const reportExists = ref(false)
const existingReportId = ref(null)
const users = computed(() => rawUsers.value.map(itm => ({
label: `${itm.FAM_V} ${itm.IM_V} ${itm.OT_V}`,
value: itm.LPUDoctorID
})))
const departments = computed(() => rawDepartments.value.map(itm => ({
label: itm.name_short,
value: itm.department_id
})))
const fetchUsers = () => {
axios.get('/api/mis/department-users')
.then((res) => {
reportStore.departmentUsers = res.data
rawUsers.value = res.data
})
}
const fetchDepartments = () => {
axios.get('/api/app/departments')
.then((res) => {
rawDepartments.value = res.data
})
}
const checkReportExists = async (userId, departmentId) => {
if (!userId || !departmentId) {
reportExists.value = false;
existingReportId.value = null;
return;
}
try {
await axios.get(`/api/report/check`, {
params: {
department_id: departmentId,
}
}).then(res => {
reportExists.value = res.data.exists;
existingReportId.value = res.data.report_id || null;
});
} catch (error) {
console.error('Ошибка при проверке отчета:', error);
reportExists.value = false;
existingReportId.value = null;
} finally {
}
}
// Дебаунс функция
const debouncedCheck = useDebounceFn((userId, departmentId) => {
checkReportExists(userId, departmentId);
}, 300);
// Автоматическая проверка при изменении любого из полей
watch(
() => [reportStore.reportInfo.userId, reportStore.reportInfo.departmentId],
([newUserId, newDepartmentId], [oldUserId, oldDepartmentId]) => {
// Проверяем, что оба поля заполнены и корректны
const userIdValid = newUserId && Number.isInteger(Number(newUserId));
const departmentIdValid = newDepartmentId && Number.isInteger(Number(newDepartmentId));
if (userIdValid && departmentIdValid) {
debouncedCheck(newUserId, newDepartmentId);
} else {
reportExists.value = false;
existingReportId.value = null;
}
},
{ deep: true }
);
const rules = {
userId: {
required: true,
validator: (rule, value) => {
if (Number.isInteger(value)) return true
return false
return Number.isInteger(value) ?? false
},
trigger: ['change', 'blur'],
message: 'Выберите ответственного'
}
},
departmentId: {
required: true,
validator: (rule, value) => {
return Number.isInteger(value) ?? false
},
trigger: ['change', 'blur'],
message: 'Выберите отделение'
},
}
onMounted(() => {
reportStore.reportInfo.userId = null
reportStore.reportInfo.departmentId = null
fetchUsers()
fetchDepartments()
})
const onSubmit = (e) => {
e.preventDefault()
formRef.value?.validate((errors) => {
if (!errors) {
router.visit(`/report?userId=${reportStore.reportInfo.userId}`)
if (reportExists.value) return
router.visit(`/report`, {
data: {
userId: reportStore.reportInfo.userId,
departmentId: reportStore.reportInfo.departmentId
}
})
}
else {
window.$message.warning('Заполните выделенные поля')
}
})
}
const onAfterLeave = () => {
reportStore.reportInfo.userId = null
reportStore.reportInfo.departmentId = null
reportExists.value = false
existingReportId.value = null
}
</script>
@@ -68,12 +151,25 @@ const onAfterLeave = () => {
filterable
/>
</NFormItem>
<NFormItem label="Выберите отделение" path="departmentId">
<NSelect :options="departments"
v-model:value="reportStore.reportInfo.departmentId"
filterable
/>
</NFormItem>
</NForm>
<NAlert v-if="reportExists" type="warning">
Сводная уже создана.
<NButton :tag="Link" text :href="`/report?userId=${reportStore.reportInfo.userId}&departmentId=${reportStore.reportInfo.departmentId}`">
Перейти
</NButton>
</NAlert>
<template #action>
<NButton form-id="select-user-form"
type="primary"
block
@click="onSubmit">
@click="onSubmit"
:disabled="reportExists">
Перейти к заполнению сводной
</NButton>
</template>

View File

@@ -0,0 +1,173 @@
<script setup>
import {
NModal,
NList,
NListItem,
NThing,
NAvatar,
NIcon,
NText,
NDivider,
NFlex,
NButton,
NScrollbar,
NEmpty,
NDataTable,
NBadge,
NForm,
NDrawerContent,
NInput,
NSpin,
NDrawer
} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import {TbAlertCircle, TbCheck, TbEye, TbGripVertical, TbPencil, TbTrashX, TbX} from 'vue-icons-plus/tb'
import {computed, h, ref, watch} from "vue";
const props = defineProps({
departmentId: {
required: true
},
startAt: {
required: true
},
endAt: {
required: true
}
})
const reportStore = useReportStore()
const open = defineModel('open')
const loading = ref(true)
const baseColumns = reportStore.getColumnsByKey(['num', 'fullname', 'age', 'birth_date', 'mkb.ds'])
const currentPatient = ref(null)
const showMoveDrawer = ref(false)
const columns = computed(() => {
// if (!isFillableMode.value) return baseColumns
const newColumns = []
const expandColumn = {
title: '',
width: '30',
render: (rowData) => {
return h(
NIcon,
{
onClick: () => {
currentPatient.value = rowData
showMoveDrawer.value = true
}
},
{
default: h(TbEye)
}
)
}
}
const fillableColumn = {
title: '',
key: 'fillable',
width: '20',
render: (row) => h(
NBadge,
{
dot: true,
color: (row.comment && row.comment.trim()) ? '#7fe7c4' : '#e88080'
}
)
}
newColumns.push(expandColumn)
newColumns.push(fillableColumn)
newColumns.push(...baseColumns)
return newColumns
})
const observablePatients = ref([])
const fetchUnwantedEvents = () => {
loading.value = true
const data = {
status: 'observation',
startAt: props.startAt,
endAt: props.endAt,
departmentId: props.departmentId
}
axios.post('/api/mis/patients', data).then((res) => {
observablePatients.value = reportStore.addRowNumbers(res.data)
}).finally(() => {
loading.value = false
})
}
watch(() => [props.departmentId, props.endAt, props.startAt], () => {
if (props.departmentId && props.endAt && props.startAt) {
fetchUnwantedEvents()
}
}, {
immediate: true,
deep: true
})
</script>
<template>
<NModal v-model:show="open"
title="Пациенты на контроле"
preset="card"
:mask-closable="false"
:close-on-esc="false"
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
id="modal-observation-patients"
>
<template v-if="loading">
<div class="flex items-center justify-center h-full">
<NSpin />
</div>
</template>
<template v-else-if="observablePatients.length">
<NDataTable :columns="columns"
ref="tableRef"
:data="observablePatients"
size="small"
:loading="loading"
max-height="200"
min-height="200"
:row-key="(row, index) => row.id"
class="text-sm!">
</NDataTable>
</template>
<template v-else>
<div class="h-full flex items-center justify-center">
<NEmpty description="Пациентов на контроле не найдено!" />
</div>
</template>
<NDrawer
v-model:show="showMoveDrawer"
placement="bottom"
:min-height="400"
:max-height="600"
:default-height="400"
resizable
:trap-focus="false"
:block-scroll="false"
to="#modal-observation-patients"
>
<NDrawerContent title="Причина постановки на контроль" closable>
<NInput type="textarea" readonly :rows="8" v-model:value="currentPatient.comment" />
</NDrawerContent>
</NDrawer>
</NModal>
</template>
<style scoped>
:deep(.n-data-table-th),
:deep(.n-data-table-td) {
white-space: nowrap !important;
font-size: var(--n-font-size);
}
</style>

View File

@@ -0,0 +1,115 @@
<script setup>
import {
NModal, NList, NListItem, NThing, NAvatar, NIcon,
NText, NDivider, NFlex, NButton, NScrollbar, NEmpty, NSpin
} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
import { TbAlertCircle, TbPencil, TbTrashX } from 'vue-icons-plus/tb'
import {ref, watch} from "vue";
const props = defineProps({
departmentId: {
required: true
},
startAt: {
required: true
},
endAt: {
required: true
}
})
const open = defineModel('open')
const loading = ref(true)
const unwantedEvents = ref([])
const fetchUnwantedEvents = () => {
loading.value = true
axios.get('/api/statistics/reports/unwanted-events', {
params: {
departmentId: props.departmentId,
startAt: props.startAt,
endAt: props.endAt
}
})
.then(res => {
unwantedEvents.value = res.data
})
.finally(() => {
loading.value = false
})
}
watch(() => [props.departmentId, props.endAt, props.startAt], () => {
if (props.departmentId && props.endAt && props.startAt) {
fetchUnwantedEvents()
}
}, {
immediate: true,
deep: true
})
</script>
<template>
<NModal v-model:show="open"
title="Нежелательные события"
preset="card"
:mask-closable="false"
:close-on-esc="false"
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
>
<template v-if="loading">
<div class="flex items-center justify-center h-full">
<NSpin />
</div>
</template>
<template v-else-if="unwantedEvents.length">
<NScrollbar class="max-h-[calc(100vh-282px)] pr-3">
<NList>
<NListItem v-for="event in unwantedEvents">
<NThing>
<template #avatar>
<NAvatar>
<NIcon>
<TbAlertCircle class="text-red-400" />
</NIcon>
</NAvatar>
</template>
<template #header>
{{ event.title }}
</template>
<template #description>
<NText depth="3">
{{ event.created_at }}
</NText>
</template>
<NText>
{{ event.comment }}
</NText>
</NThing>
</NListItem>
</NList>
</NScrollbar>
</template>
<template v-else>
<div class="h-full flex items-center justify-center">
<NEmpty description="Нежелательные события не найдены!" />
</div>
</template>
<!-- <template #action>-->
<!-- <NFlex id="modal-action" align="center" justify="space-between">-->
<!-- <NButton type="primary" secondary @click="onCreateEvent()">-->
<!-- <template #icon>-->
<!-- <TbCirclePlus />-->
<!-- </template>-->
<!-- Создать событие-->
<!-- </NButton>-->
<!-- </NFlex>-->
<!-- </template>-->
</NModal>
</template>
<style scoped>
</style>

View File

@@ -1,9 +1,12 @@
<script setup>
import {NDataTable, NFlex, NText, NDatePicker} from 'naive-ui'
import {NDataTable, NFlex, NText, NDatePicker, NBadge, NIcon, NPopover, NTag, NSpace} from 'naive-ui'
import AppLayout from "../../Layouts/AppLayout.vue";
import {h, ref} from "vue";
import DatePickerQuery from "../../Components/DatePickerQuery.vue";
import {Link, usePage} from "@inertiajs/vue3";
import {TbAlertCircle, TbEye} from "vue-icons-plus/tb";
import ModalUnwantedEvents from "./Components/ModalUnwantedEvents.vue";
import ModalObservablePatients from "./Components/ModalObservablePatients.vue";
const props = defineProps({
data: {
@@ -37,7 +40,7 @@ const columns = ref([
}
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.department)
return row.department
}
// Получаем текущие query параметры
@@ -46,8 +49,10 @@ const columns = ref([
const searchParams = currentUrl.searchParams
// Берем startAt и endAt из текущего URL
const startAt = searchParams.get('startAt')
const endAt = searchParams.get('endAt')
const propsStartAt = Array.isArray(props.date) ? props.date[0] : props.date
const propsEndAt = Array.isArray(props.date) ? props.date[1] : props.date
const startAt = searchParams.get('startAt') ?? propsStartAt
const endAt = searchParams.get('endAt') ?? propsEndAt
const linkData = {}
@@ -56,11 +61,53 @@ const columns = ref([
if (endAt)
linkData.endAt = endAt
return h(Link, {
href: `/report`,
data: linkData,
class: 'underline decoration-dashed'
}, row.department)
return h(NFlex, {align: 'center', justify: 'start'}, [
h(NBadge, {
dot: true,
type: row.isReportToday ? 'success' : 'error'
}),
h(Link, {
href: `/report`,
data: linkData,
class: 'underline decoration-dashed'
}, row.department),
h(NSpace, {align: 'center', size: 'small'}, [
h(NPopover, {
trigger: 'hover',
}, {
trigger: h(NTag, {
round: true,
size: 'small',
bordered: false,
class: 'cursor-pointer!',
onClick: () => onShowUnwantedEventsModal(row.department_id)
}, {
icon: h(NIcon, { }, {
default: () => h(TbAlertCircle)
}),
default: row.countUnwanted
}),
default: 'Нежелательные события'
}),
h(NPopover, {
trigger: 'hover',
}, {
trigger: h(NTag, {
round: true,
size: 'small',
bordered: false,
class: 'cursor-pointer!',
onClick: () => onShowObservablePatientsModal(row.department_id)
}, {
icon: h(NIcon, { }, {
default: () => h(TbEye)
}),
default: row.countObservable
}),
default: 'Пациенты на контроле'
}),
])
])
}
},
{
@@ -69,13 +116,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.beds)
} else {
return h(NText, { }, row.beds)
}
}
},
{
title: 'Поступило',
@@ -88,13 +128,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.all)
} else {
return h(NText, { }, row.recipients.all)
}
}
},
{
title: 'План',
@@ -102,13 +135,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.plan)
} else {
return h(NText, { }, row.recipients.plan)
}
}
},
{
title: 'Экстр',
@@ -116,13 +142,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.emergency)
} else {
return h(NText, { }, row.recipients.emergency)
}
}
},
{
title: 'Перевод',
@@ -130,13 +149,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.recipients.transferred)
} else {
return h(NText, { }, row.recipients.transferred)
}
}
},
]
},
@@ -146,13 +158,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.outcome)
} else {
return h(NText, { }, row.outcome)
}
}
},
{
title: 'Состоит',
@@ -160,13 +165,13 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.consist)
} else {
return h(NText, { }, row.consist)
}
}
},
{
title: 'Ср. койко-день',
key: 'averageBedDays',
width: 44,
titleAlign: 'center',
align: 'center',
},
{
title: '% загруженности',
@@ -174,13 +179,6 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.percentLoadedBeds)
} else {
return h(NText, { }, row.percentLoadedBeds)
}
}
},
{
title: 'Операции',
@@ -193,13 +191,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.surgical.emergency)
} else {
return h(NText, { }, row.surgical.emergency)
}
}
},
{
title: 'П',
@@ -207,13 +198,6 @@ const columns = ref([
width: 60,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.surgical.plan)
} else {
return h(NText, { }, row.surgical.plan)
}
}
},
]
},
@@ -223,24 +207,41 @@ const columns = ref([
width: 84,
titleAlign: 'center',
align: 'center',
render: (row) => {
if (row.isTotalRow) {
return h(NText, { style: 'font-weight: 600;' }, row.deceased)
} else {
return h(NText, { }, row.deceased)
}
}
},
{
title: 'Мед. персонал',
key: 'countStaff',
width: 84,
titleAlign: 'center',
align: 'center',
},
])
const currentDepartmentId = ref(null)
const showUnwantedEventsModal = ref(false)
const showObservablePatientsModal = ref(false)
const onShowUnwantedEventsModal = (departmentId) => {
currentDepartmentId.value = departmentId
showUnwantedEventsModal.value = true
}
const onShowObservablePatientsModal = (departmentId) => {
currentDepartmentId.value = departmentId
showObservablePatientsModal.value = true
}
const rowProps = (row) => {
if (row.isGroupHeader) return {
style: `--n-merged-td-color: var(--n-merged-th-color)`
}
if (row.isTotalRow) return {
style: `--n-merged-td-color: var(--n-merged-th-color); --n-text-color: var(--n-th-icon-color-active);`
style: `--n-merged-td-color: var(--n-merged-th-color);`
}
}
const rowClassName = (row) => {
if (row.isTotalRow)
return `total-row`
}
</script>
<template>
@@ -256,9 +257,12 @@ const rowProps = (row) => {
min-height="calc(100vh - 48px - 70px)"
max-height="calc(100vh - 48px - 70px)"
:row-props="rowProps"
:row-class-name="rowClassName"
>
</NDataTable>
<ModalUnwantedEvents v-model:open="showUnwantedEventsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
<ModalObservablePatients v-model:open="showObservablePatientsModal" :start-at="date[0]" :end-at="date[1]" :department-id="currentDepartmentId" />
</AppLayout>
</template>
@@ -267,4 +271,9 @@ const rowProps = (row) => {
:deep(.n-data-table-td) {
font-size: var(--n-font-size);
}
:deep(.total-row td) {
--n-td-text-color: var(--n-th-icon-color-active);
font-weight: 500;
}
</style>

View File

@@ -5,6 +5,7 @@ import {router} from "@inertiajs/vue3";
export const useReportStore = defineStore('reportStore', () => {
const timestampNow = useTimestamp()
const reportFormRef = ref(null)
const _timestampCurrent = ref(null)
const timestampCurrent = computed({
@@ -93,7 +94,7 @@ export const useReportStore = defineStore('reportStore', () => {
return result
}
const sendReportForm = (assignForm) => {
const sendReportForm = (assignForm = null) => {
const form = {
metrics: reportForm.value,
observationPatients: patientsData.value['observation'],
@@ -103,6 +104,7 @@ export const useReportStore = defineStore('reportStore', () => {
timestampCurrentRange.value[1]
],
userId: reportInfo.value.report.userId,
departmentId: reportInfo.value.department.department_id,
reportId: reportInfo.value.report.report_id,
...assignForm
}
@@ -206,6 +208,7 @@ export const useReportStore = defineStore('reportStore', () => {
}
return {
reportFormRef,
timestampNow,
timestampCurrent,
timestampCurrentRange,