* добавлены операции и услуги операций

* добавлена выборка и подсчет по датам для роли зав.
* переключатель ролей
* выбор отделений для роли зав.
This commit is contained in:
brusnitsyn
2026-01-22 17:58:27 +09:00
parent 8a0fdf9470
commit cb43c74a72
28 changed files with 961 additions and 143 deletions

View File

@@ -0,0 +1,22 @@
<script setup>
import {useAuthStore} from "../Stores/auth.js";
import {NSelect} from "naive-ui";
import {computed, ref} from "vue";
const authStore = useAuthStore()
const value = ref(authStore.userDepartment.department_id)
const departments = computed(() => authStore.availableDepartments.map(item => ({
label: item.name_full,
value: item.department_id
})))
</script>
<template>
<NSelect v-model:value="value" :options="departments" class="max-w-[280px]" />
</template>
<style scoped>
</style>

View File

@@ -23,10 +23,10 @@ const { timestampCurrentRange } = storeToRefs(reportStore)
<template>
<NDatePicker :theme-overrides="themeOverride"
v-model:value="timestampCurrentRange"
v-model:value="reportStore.timestampCurrentRange"
format="dd.MM.YYYY"
type="daterange"
@update-value="reportStore.getDataOnReportDate"
@update-value="value => reportStore.getDataOnReportDate(value)"
/>
</template>

View File

@@ -7,18 +7,13 @@ import AppHeaderRole from "./AppHeaderRole.vue";
</script>
<template>
<NFlex justify="space-between" align="center" class="px-4 w-full h-full">
<NFlex align="center">
<NButton :tag="Link" text href="/">
Метрика
</NButton>
<!-- <NDivider vertical />-->
<!-- <ReportSelectDate />-->
</NFlex>
<NFlex align="center">
<AppUserButton />
</NFlex>
</NFlex>
<div class="grid grid-cols-[auto_1fr_auto] px-4 w-full h-full">
<NButton :tag="Link" text href="/">
Метрика
</NButton>
<div></div>
<AppUserButton />
</div>
</template>
<style scoped>

View File

@@ -1,14 +1,35 @@
<script setup>
import {useAuthStore} from "../../Stores/auth.js";
import {NDropdown, NButton, NText} from 'naive-ui'
import {NSelect, NFlex, NText} from 'naive-ui'
import {computed} from "vue";
import {router, useForm} from "@inertiajs/vue3";
const authStore = useAuthStore()
const userOptions = [
{
label: 'Выход',
key: 'exit',
},
]
const userOptions = computed(() => {
return authStore.availableRoles.map(itm => {
return {
label: itm.name,
value: itm.role_id
}
})
})
const formRole = useForm({
role_id: authStore.user.role.role_id
})
const onChangeRole = (roleId) => {
console.log(roleId)
formRole.post('/user/role/change', {
onSuccess: () => {
router.visit(window.location.pathname, {
preserveScroll: true,
preserveState: false, // Это важно - сбрасывает состояние
only: ['user', 'auth'], // Указываем какие данные обновить
})
}
})
}
const themeOverride = {
border: null,
borderHover: null,
@@ -20,9 +41,17 @@ const themeOverride = {
</script>
<template>
<NText>
{{ authStore.user?.name }}
</NText>
<NFlex align="center" :wrap="false">
<div class="min-w-[220px]">
<NSelect :options="userOptions"
v-model:value="formRole.role_id"
@update:value="value => onChangeRole(value)"
/>
</div>
<div>
{{ authStore.user?.name }}
</div>
</NFlex>
<!-- <NDropdown :options="userOptions" placement="bottom-end">-->
<!-- <NButton :theme-overrides="themeOverride">-->
<!-- {{ authStore.user?.name }}-->

View File

@@ -27,7 +27,7 @@ const onSubmit = () => {
<NFlex vertical class="max-w-6xl mx-auto mt-6 mb-4 w-full">
<ReportHeader :mode="mode" />
<ReportFormInput />
<ReportFormInput v-if="mode === 'fillable'" />
<ReportSection label="Планово" />

View File

@@ -8,6 +8,9 @@ import {useAuthStore} from "../../../Stores/auth.js";
import {storeToRefs} from "pinia";
import {RiAddCircleLine} from 'vue-icons-plus/ri'
import {useReportStore} from "../../../Stores/report.js";
import ReportSelectDate from "../../../Components/ReportSelectDate.vue";
import DepartmentSelect from "../../../Components/DepartmentSelect.vue";
import UnwantedEventModal from "./UnwantedEventModal.vue";
const props = defineProps({
mode: {
@@ -34,6 +37,8 @@ const {reportInfo} = storeToRefs(reportStore)
const isFillableMode = computed(() => props.mode.toLowerCase() === 'fillable')
const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
const openUnwantedEventModal = ref(false)
const selectDepartment = ref(0)
const currentDate = computed(() => {
@@ -48,17 +53,17 @@ const currentDate = computed(() => {
<template>
<NCard>
<NFlex vertical>
<NFlex align="center" justify="space-between">
<NH2 v-if="isFillableMode" class="mb-0!">
{{ currentDate }}
</NH2>
<NDatePicker v-if="isReadonlyMode" />
<NFlex align="center" justify="space-between" :wrap="false">
<NTag v-if="isFillableMode" type="info" :bordered="false">
{{ authStore.userDepartment.name_full }}
</NTag>
<DepartmentSelect v-if="isReadonlyMode" />
<NFlex align="center" :wrap="false">
<NTag v-if="isFillableMode" type="info" :bordered="false">
{{ authStore.userDepartment.name_full }}
</NTag>
<NSelect v-if="isReadonlyMode" v-model:value="selectDepartment" :options="departments" />
<NH2 v-if="isFillableMode" class="mb-0!">
{{ currentDate }}
</NH2>
<ReportSelectDate v-if="isReadonlyMode" />
</NFlex>
</NFlex>
@@ -80,9 +85,33 @@ const currentDate = computed(() => {
</template>
</NStatistic>
</NCol>
<NCol :span="5">
<NStatistic label="Поступило">
<template #default>
<NSkeleton v-if="reportStore.isLoadReportInfo" round class="w-[70px]! mt-2 h-[29px]!" />
<span v-else>{{ reportStore.reportInfo?.department.recipientCount }}</span>
</template>
</NStatistic>
</NCol>
<NCol :span="5">
<NStatistic label="Выбыло">
<template #default>
<NSkeleton v-if="reportStore.isLoadReportInfo" round class="w-[70px]! mt-2 h-[29px]!" />
<span v-else>{{ reportStore.reportInfo?.department.extractCount }}</span>
</template>
</NStatistic>
</NCol>
<NCol :span="5">
<NStatistic label="Состоит">
<template #default>
<NSkeleton v-if="reportStore.isLoadReportInfo" round class="w-[70px]! mt-2 h-[29px]!" />
<span v-else>{{ reportStore.reportInfo?.department.currentCount }}</span>
</template>
</NStatistic>
</NCol>
</NRow>
<NButton type="primary" secondary>
<NButton type="error" secondary @click="openUnwantedEventModal = true">
<template #icon>
<RiAddCircleLine />
</template>
@@ -91,6 +120,8 @@ const currentDate = computed(() => {
</NFlex>
</NFlex>
</NCard>
<UnwantedEventModal v-model:open="openUnwantedEventModal" />
</template>
<style scoped>

View File

@@ -17,7 +17,7 @@ const reportStore = useReportStore()
const {patientsData} = storeToRefs(reportStore)
const handleItemDragged = (event) => {
console.log('Начато перетаскивание:', event)
// console.log('Начато перетаскивание:', event)
}
// Обработка события drop
@@ -47,6 +47,8 @@ const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
<ReportSectionHeader title="Планово" status="plan" />
</template>
<ReportSectionItem status="plan"
:accent-ids="reportStore.reportInfo?.department.recipientIds"
is-draggable
@item-dragged="handleItemDragged"
/>
</NCollapseItem>
@@ -55,17 +57,20 @@ const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
<ReportSectionHeader title="Экстренно" status="emergency" />
</template>
<ReportSectionItem status="emergency"
:accent-ids="reportStore.reportInfo?.department.recipientIds"
is-draggable
@item-dragged="handleItemDragged"
/>
</NCollapseItem>
<NCollapseItem name="3">
<template #header>
<ReportSectionHeader title="Наблюдение" status="observation" />
<ReportSectionHeader title="Находятся на контроле" status="observation" />
</template>
<NFlex :size="12">
<ReportSectionItem status="observation"
@item-dragged="handleItemDragged"
@item-dropped="handleItemDropped"
is-removable
/>
<NAlert v-if="isFillableMode" type="info" class="w-full">
Перетаскивайте строки из верхних таблиц, что бы добавить в наблюдение

View File

@@ -1,6 +1,7 @@
<script setup>
import {computed, onMounted, ref} from "vue";
import {computed, onMounted, ref, watch} from "vue";
import {NSkeleton, NText} from 'naive-ui'
import {useReportStore} from "../../../Stores/report.js";
const props = defineProps({
title: {
@@ -13,14 +14,19 @@ const props = defineProps({
}
})
const reportStore = useReportStore()
const isLoading = ref(true)
const countPatient = ref(null)
const fetchPatientCount = async () => {
if (props.status === 'plan' || props.status === 'emergency') {
isLoading.value = true
await axios.post('/api/mis/patients/count', {
status: props.status
}).then((res) => {
const data = {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1]
}
await axios.post('/api/mis/patients/count', data).then((res) => {
countPatient.value = res.data
}).finally(() => {
isLoading.value = false
@@ -41,6 +47,10 @@ const computedHeader = computed(() => {
onMounted(async () => {
await fetchPatientCount()
})
watch(() => reportStore.timestampCurrentRange, (newRange) => {
if (newRange) fetchPatientCount()
})
</script>
<template>

View File

@@ -1,5 +1,5 @@
<script setup>
import {NIcon, NDataTable} from "naive-ui";
import {NIcon, NText, NDataTable, NButton} from "naive-ui";
import {useReportStore} from "../../../Stores/report.js";
import {computed, h, onMounted, ref, watch} from "vue";
import { VueDraggableNext } from 'vue-draggable-next'
@@ -18,12 +18,26 @@ const props = defineProps({
status: {
type: String,
default: null // 'plan'
},
isRemovable: {
type: Boolean,
default: false
},
isDraggable: {
type: Boolean,
default: false
},
accentIds: {
type: Array,
default: []
}
})
const isFillableMode = computed(() => props.mode.toLowerCase() === 'fillable')
const isReadonlyMode = computed(() => props.mode.toLowerCase() === 'readonly')
const tableRef = ref()
const emit = defineEmits(['item-dragged', 'item-dropped'])
const reportStore = useReportStore()
@@ -37,6 +51,8 @@ const isLoading = ref(true)
const columns = computed(() => {
if (!isFillableMode.value) return baseColumns
const newColumns = []
const dragColumn = {
title: '',
key: 'drag',
@@ -59,7 +75,46 @@ const columns = computed(() => {
)
}
return [dragColumn, ...baseColumns]
const removeColumn = {
title: '',
key: 'remove',
render: (row) => h(
NButton,
{
text: true,
onClick: () => {
alert('message')
}
},
[
'Снять с наблюдения'
]
)
}
if (props.isDraggable) newColumns.push(dragColumn)
newColumns.push(...baseColumns)
if (props.isRemovable) newColumns.push(removeColumn)
if (props.status === 'emergency') {
const operationColumn = {
title: 'Операции',
key: 'operations',
render: (row) => row.operations.length ?
h(
NText,
{},
[
row.operations.map(itm => {
return `${itm.code}; `
})
]
) : h('div', {}, '-')
}
newColumns.push(operationColumn)
}
return newColumns
})
const handleDragStart = (e, row) => {
@@ -108,9 +163,12 @@ const handleDrop = (e) => {
const fetchPatients = async () => {
isLoading.value = true
await axios.post('/api/mis/patients', {
status: props.status
}).then((res) => {
const data = {
status: props.status,
startAt: reportStore.timestampCurrentRange[0],
endAt: reportStore.timestampCurrentRange[1],
}
await axios.post('/api/mis/patients', data).then((res) => {
patientsData.value[props.status] = res.data
}).finally(() => {
isLoading.value = false
@@ -118,19 +176,35 @@ const fetchPatients = async () => {
}
function rowProps(row) {
const style = []
const classes = []
style.push(props.isDraggable ? 'cursor: grab;' : 'cursor: arrow;')
if (props.accentIds.length) {
console.log(props.accentIds.includes(row.id))
if (props.accentIds.includes(row.id)) {
style.push('--n-merged-td-color: #047857')
}
}
return {
draggable: true,
style: 'cursor: grab;',
draggable: props.isDraggable,
style: style,
onDragstart: (e) => {
if (!props.isDraggable) return
handleDragStart(e, row)
},
onDragend: (e) => {
if (!props.isDraggable) return
handleDragEnd(e)
},
onDragover: (e) => {
if (!props.isDraggable) return
handleDragOver(e)
},
onDrop: (e) => {
if (!props.isDraggable) return
handleDrop(e)
}
}
@@ -143,6 +217,7 @@ onMounted(async () => {
<template>
<NDataTable :columns="columns"
ref="tableRef"
:data="patientsData[status]"
size="small"
@drop="handleDrop"

View File

@@ -0,0 +1,50 @@
<script setup>
import {NModal, NForm, NFormItem, NInput, NFlex, NButton} from 'naive-ui'
import {useForm} from "@inertiajs/vue3";
import {useReportStore} from "../../../Stores/report.js";
import {ref} from "vue";
const open = defineModel('open')
const reportStore = useReportStore()
const formRef = ref()
const rules = {
comment: {
required: true,
message: 'Заполните этот блок',
trigger: 'blur'
}
}
const onSubmit = () => {
formRef.value?.validate((errors) => {
if (!errors) {
open.value = false
}
else {
}
})
}
</script>
<template>
<NModal v-model:show="open" title="Нежелательное событие" preset="card" class="max-w-xl">
<NForm ref="formRef" :model="reportStore.reportForm" :rules="rules">
<NFormItem :show-label="false" path="comment">
<NInput type="textarea" :rows="8" v-model:value="reportStore.reportForm.comment" />
</NFormItem>
</NForm>
<template #action>
<NFlex align="center" justify="end">
<NButton type="primary" tertiary @click="onSubmit">
Сохранить
</NButton>
</NFlex>
</template>
</NModal>
</template>
<style scoped>
</style>

View File

@@ -2,17 +2,26 @@
import AppLayout from "../../Layouts/AppLayout.vue";
import ReportForm from "./Components/ReportForm.vue";
import {useReportStore} from "../../Stores/report.js";
import {onMounted} from "vue";
import {computed, onMounted} from "vue";
import {useAuthStore} from "../../Stores/auth.js";
const reportStore = useReportStore()
const authStore = useAuthStore()
onMounted(async () => {
await reportStore.getReportInfo()
})
const mode = computed(() => {
if (authStore.isHeadOfDepartment)
return 'readonly'
return 'fillable'
})
</script>
<template>
<AppLayout>
<ReportForm />
<ReportForm :mode />
</AppLayout>
</template>

View File

@@ -1,13 +1,23 @@
import { ref, computed } from 'vue'
import {ref, computed, watch} from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
import {usePage} from "@inertiajs/vue3";
export const useAuthStore = defineStore('authStore', () => {
const user = usePage().props.user
const token = user?.token
const permissions = user?.permissions
const availableDepartments = ref(user?.available_departments)
const page = usePage()
const user = ref(page.props.user)
const token = computed(() => user.value?.token)
const role = computed(() => user.value?.role)
const permissions = computed(() => user.value?.permissions)
const availableDepartments = computed(() => user.value?.available_departments)
const availableRoles = computed(() => user.value?.available_roles)
watch(
() => page.props.user,
(newUser) => {
user.value = newUser
},
{ deep: true, immediate: true })
// Инициализация axios с токеном
if (token?.value) {
@@ -16,12 +26,10 @@ export const useAuthStore = defineStore('authStore', () => {
// Вычисляемые свойства
const isAuthenticated = computed(() => !!user.value && !!token.value)
const isAdmin = computed(() => user.role === 'admin')
const isDoctor = computed(() => user.role === 'doctor')
const isNurse = computed(() => user.role === 'nurse')
const isHeadOfDepartment = computed(() => user.role === 'head_of_department')
const isStatistician = computed(() => user.role === 'statistician')
const userDepartment = computed(() => user.current_department || '')
const isAdmin = computed(() => role.value?.slug === 'admin')
const isDoctor = computed(() => role.value?.slug === 'doctor')
const isHeadOfDepartment = computed(() => role.value?.slug === 'head_of_department')
const userDepartment = computed(() => user.value?.current_department || '')
const clearAuthData = () => {
user.value = null
@@ -59,12 +67,11 @@ export const useAuthStore = defineStore('authStore', () => {
token,
permissions,
availableDepartments,
availableRoles,
isAuthenticated,
isAdmin,
isDoctor,
isNurse,
isHeadOfDepartment,
isStatistician,
userDepartment,
clearAuthData,

View File

@@ -19,7 +19,7 @@ export const useReportStore = defineStore('reportStore', () => {
}
})
const timestampCurrentRange = ref([timestampNow.value, timestampNow.value])
const timestampCurrentRange = ref([null, null])
const dataOnReport = ref(null)
@@ -74,6 +74,9 @@ export const useReportStore = defineStore('reportStore', () => {
const form = {
metrics: reportForm.value,
observationPatients: patientsData.value['observation'],
unwantedEvent: {
comment: reportForm.comment
},
...assignForm
}
@@ -99,21 +102,40 @@ export const useReportStore = defineStore('reportStore', () => {
await axios.get('/api/report')
.then((res) => {
reportInfo.value = res.data
reportForm.value.metrika_item_3 = reportInfo.value.department?.recipientCount
reportForm.value.metrika_item_7 = reportInfo.value.department?.extractCount
reportForm.value.metrika_item_8 = reportInfo.value.department?.currentCount
timestampCurrentRange.value = [
reportInfo.value.dates.startAt,
reportInfo.value.dates.endAt,
]
})
.finally(() => {
isLoadReportInfo.value = false
})
}
const getDataOnReportDate = async () => {
await axios.get(`/api/metric-forms/1/report-by-date?sent_at=${timestampCurrentRange.value}`)
.then(res => {
dataOnReport.value = res.data
})
.catch(err => {
// Отчета на выбранную дату не найдено
if (err.code === 404) {}
})
const getDataOnReportDate = async (dateRange) => {
isLoadReportInfo.value = true
timestampCurrentRange.value = dateRange
await axios.get(`/api/report?startAt=${timestampCurrentRange.value[0]}&endAt=${timestampCurrentRange.value[1]}`)
.then((res) => {
reportInfo.value = res.data
reportForm.value.metrika_item_3 = reportInfo.value.department?.recipientCount
reportForm.value.metrika_item_7 = reportInfo.value.department?.extractCount
reportForm.value.metrika_item_8 = reportInfo.value.department?.currentCount
timestampCurrentRange.value = [
reportInfo.value.dates.startAt,
reportInfo.value.dates.endAt,
]
})
.finally(() => {
isLoadReportInfo.value = false
})
}
return {