Обновлен стартовый экран
Переписаны запросы для статистики, отчетов Добавлена интеграция отчета сестры
This commit is contained in:
@@ -40,7 +40,7 @@ const actionGroups = computed(() => [
|
||||
key: 'actions',
|
||||
items: [
|
||||
{
|
||||
label: 'Добавить на контроль',
|
||||
label: 'Поставить на контроль',
|
||||
key: 'add-observable',
|
||||
icon: TbEyePlus,
|
||||
if: props.isAddObservable
|
||||
@@ -50,12 +50,7 @@ const actionGroups = computed(() => [
|
||||
key: 'add-observable-comment',
|
||||
icon: TbMessage,
|
||||
if: props.isAddObservableComment
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'danger',
|
||||
items: [
|
||||
},
|
||||
{
|
||||
label: 'Снять с контроля',
|
||||
key: 'remove-observable',
|
||||
@@ -88,9 +83,9 @@ const onSelectOption = (key) => {
|
||||
// props.onAddObservable?.(props.row)
|
||||
emits('addObservable', props.row)
|
||||
},
|
||||
'add-comment': () => {
|
||||
'add-observable-comment': () => {
|
||||
// props.onAddObservableComment?.(props.row)
|
||||
emits('onAddObservableComment', props.row)
|
||||
emits('addObservableComment', props.row)
|
||||
},
|
||||
'remove-observable': () => {
|
||||
// props.onRemoveObservable?.(props.row)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { NTooltip } from 'naive-ui'
|
||||
import {computed} from "vue";
|
||||
const props = defineProps({
|
||||
operations: Array
|
||||
operations: Array,
|
||||
patient: Object
|
||||
})
|
||||
|
||||
const emits = defineEmits(['click'])
|
||||
@@ -13,7 +14,7 @@ const firstOperation = computed(() => props.operations[0])
|
||||
<template>
|
||||
<NTooltip v-if="operations.length" :arrow="false">
|
||||
<template #trigger>
|
||||
<div class="absolute inset-0 p-2 pt-2.5 cursor-pointer" @click="emits('click', operations)">
|
||||
<div class="absolute inset-0 p-2 pt-2.5 cursor-pointer" @click="emits('click', {patient, operations})">
|
||||
{{ firstOperation?.code_service }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
287
resources/js/Pages/Report/Components/DutyPatientsPane.vue
Normal file
287
resources/js/Pages/Report/Components/DutyPatientsPane.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<script setup>
|
||||
import PatientTypeSection from "./PatientSection.vue";
|
||||
import PatientDataTable from "./PatientDataTable.vue";
|
||||
import {NButton, NFlex, NInput, NTabPane, NTabs, NSpace, NDivider} from "naive-ui";
|
||||
import PatientTypeSectionItem from "./PatientSectionItem.vue";
|
||||
import {computed, inject, ref} from "vue";
|
||||
import {usePatientColumns} from "../../../Composables/usePatientColumns.js";
|
||||
import OperationInfoModal from "./OperationInfoModal.vue";
|
||||
import ObservableModal from "./Modals/ObservableModal.vue";
|
||||
import {useAppDialog} from "../../../Composables/useAppDialog.js";
|
||||
import {TbSearch} from "vue-icons-plus/tb";
|
||||
import {router, usePage} from "@inertiajs/vue3";
|
||||
|
||||
const props = defineProps({
|
||||
patients: {
|
||||
type: Object,
|
||||
default: () => ({data: []})
|
||||
},
|
||||
nursePatients: {
|
||||
type: Object,
|
||||
default: () => ({data: []})
|
||||
}
|
||||
})
|
||||
const emits = defineEmits([
|
||||
'createObservable',
|
||||
'removeObservable',
|
||||
])
|
||||
|
||||
const operationsInModal = ref(null)
|
||||
const showOperationsModal = ref(false)
|
||||
const activePatient = ref(null)
|
||||
const showObservableModal = ref(false)
|
||||
const searchArg = ref(null)
|
||||
const searching = ref(false)
|
||||
const { reportForm, updateReportForm } = inject('reportForm')
|
||||
|
||||
const page = usePage()
|
||||
const canEditObservable = computed(() => {
|
||||
const perms = page.props.user?.permissions ?? []
|
||||
return perms.includes('report.create') || perms.includes('report.edit') || perms.includes('report.edit.past')
|
||||
})
|
||||
|
||||
// Нормализация даты до минут для сравнения (убирает расхождения в секундах)
|
||||
const normDate = (d) => {
|
||||
if (!d) return null
|
||||
try { return new Date(d).toISOString().slice(0, 16) } catch { return null }
|
||||
}
|
||||
|
||||
const nurseByOriginalId = computed(() => {
|
||||
const map = new Map()
|
||||
for (const p of props.nursePatients?.data ?? []) {
|
||||
if (p.original_id != null) map.set(p.original_id, p)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const FIELD_LABELS = {
|
||||
recipient_date: 'дата госпитализации',
|
||||
extract_date: 'дата выписки',
|
||||
death_date: 'дата смерти',
|
||||
ingoing_date: 'дата поступления в отделение',
|
||||
out_date: 'дата выбытия из отделения',
|
||||
department: 'перевод в другое отделение',
|
||||
}
|
||||
|
||||
const getNurseChanges = (misPat) => {
|
||||
// Текущий период: MedicalHistory использует 'id', прошлый: ReportDutyPatient — 'original_id'
|
||||
const patientKey = misPat.original_id ?? misPat.id
|
||||
const nurseP = nurseByOriginalId.value.get(patientKey)
|
||||
if (!nurseP) return []
|
||||
|
||||
const changes = []
|
||||
|
||||
if (normDate(nurseP.recipient_date) !== normDate(misPat.recipient_date))
|
||||
changes.push('recipient_date')
|
||||
if (normDate(nurseP.extract_date) !== normDate(misPat.extract_date))
|
||||
changes.push('extract_date')
|
||||
if (normDate(nurseP.death_date) !== normDate(misPat.death_date))
|
||||
changes.push('death_date')
|
||||
|
||||
// Текущий период грузит latestMigration (объект), прошлый — migrations (массив)
|
||||
const nurseMig = nurseP.migrations?.[0]
|
||||
const misMig = misPat.migrations?.[0] ?? misPat.latest_migration
|
||||
if (nurseMig && misMig) {
|
||||
if (normDate(nurseMig.ingoing_date) !== normDate(misMig.ingoing_date))
|
||||
changes.push('ingoing_date')
|
||||
if (normDate(nurseMig.out_date) !== normDate(misMig.out_date))
|
||||
changes.push('out_date')
|
||||
if (nurseMig.department_id !== misMig.department_id)
|
||||
changes.push('department')
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
const withNurse = (p) => {
|
||||
const nurse_changes = getNurseChanges(p)
|
||||
if (!nurse_changes.length) return p
|
||||
return {
|
||||
...p,
|
||||
nurse_changes,
|
||||
nurse_changes_labels: nurse_changes.map(k => FIELD_LABELS[k] ?? k),
|
||||
}
|
||||
}
|
||||
|
||||
const patientsByGroup = computed(() => {
|
||||
const groups = {
|
||||
urgent: [],
|
||||
planned: [],
|
||||
deceased: [],
|
||||
in_department: [],
|
||||
recipient: [],
|
||||
discharged: [],
|
||||
transferred: [],
|
||||
reanimations: [],
|
||||
observables: []
|
||||
}
|
||||
|
||||
const patients = props.patients?.data ?? []
|
||||
|
||||
for (const raw of patients) {
|
||||
const p = withNurse(raw)
|
||||
const flags = p.period_flags ?? {}
|
||||
const isCurrentAtEnd = flags.current_at_end ?? ['in_department', 'recipient'].includes(p.patient_status)
|
||||
|
||||
// Группировка по срочности за период: пациент должен состоять на конец периода.
|
||||
if (isCurrentAtEnd && (flags.urgent ?? p.patient_urgency === 'urgent')) groups.urgent.push(p)
|
||||
else if (isCurrentAtEnd && (flags.planned ?? p.patient_urgency === 'planned')) groups.planned.push(p)
|
||||
|
||||
// Группировка по реанимации
|
||||
if (p.in_reanimation === true) groups.reanimations.push(p)
|
||||
// Группировка по наблюдению
|
||||
if (p.in_observable === true) groups.observables.push(p)
|
||||
|
||||
// Событийная группировка за период. Один пациент может быть в нескольких группах.
|
||||
if (flags.recipient ?? p.patient_status === 'recipient') groups.recipient.push(p)
|
||||
if (flags.current_at_end ?? p.patient_status === 'in_department') groups.in_department.push(p)
|
||||
if (flags.discharged ?? p.patient_status === 'discharged') groups.discharged.push(p)
|
||||
if (flags.deceased ?? p.patient_status === 'deceased') groups.deceased.push(p)
|
||||
if (flags.transferred ?? p.patient_status === 'transferred') groups.transferred.push(p)
|
||||
}
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
const {
|
||||
planOrEmergencyColumns, observableColumns, reanimationColumns, dischargedColumns,
|
||||
deceasedColumns, transferredColumns,
|
||||
} = usePatientColumns({
|
||||
onAddObservable: (row) => {
|
||||
activePatient.value = row
|
||||
showObservableModal.value = true
|
||||
},
|
||||
onShowOperationModal: ({patient, operations}) => {
|
||||
activePatient.value = patient
|
||||
operationsInModal.value = operations
|
||||
showOperationsModal.value = true
|
||||
},
|
||||
onAddObservableComment: (row) => {
|
||||
activePatient.value = row
|
||||
showObservableModal.value = true
|
||||
},
|
||||
onRemoveObservable: async (row) => {
|
||||
await useAppDialog({
|
||||
title: 'Снятие с контроля',
|
||||
content: 'Это действие необратимо. Продолжить?',
|
||||
onConfirm: async () => {
|
||||
const queryString = window.location.search;
|
||||
const params = new URLSearchParams(queryString);
|
||||
const url = new URL(`${window.location.origin}/api/duty/observable/close`)
|
||||
url.searchParams.append('startAt', params.get('startAt'))
|
||||
url.searchParams.append('endAt', params.get('endAt'))
|
||||
|
||||
try {
|
||||
await axios.post(url.toString(), { ...row })
|
||||
} catch (e) {
|
||||
if (e.response?.status !== 404) throw e
|
||||
// 404 — запись не найдена, удаляем локально
|
||||
}
|
||||
emits('removeObservable', row)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, { canEditObservable })
|
||||
|
||||
const findPatient = () => {
|
||||
if (searching.value) return
|
||||
|
||||
// Просто перезагружаем текущую страницу с новым search параметром
|
||||
router.reload({
|
||||
data: {
|
||||
search: searchArg.value || null, // Отправляем как POST или GET параметр
|
||||
},
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
only: ['patients', 'search'], // Обновляем только нужные пропсы
|
||||
onStart: () => {
|
||||
searching.value = true
|
||||
},
|
||||
onFinish: () => {
|
||||
searching.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
searchArg.value = null
|
||||
|
||||
router.reload({
|
||||
data: { search: null }, // Отправляем null
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
only: ['patients', 'search', 'meta'],
|
||||
onStart: () => {
|
||||
searching.value = true
|
||||
},
|
||||
onFinish: () => {
|
||||
searching.value = false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onObservablePatient = (model) => {
|
||||
const observableKey = 'observables'
|
||||
const observablesInReport = reportForm.value[observableKey]
|
||||
const preparedModel = {
|
||||
...model,
|
||||
in_observable: true
|
||||
}
|
||||
const form = { [observableKey]: [...observablesInReport, preparedModel] }
|
||||
updateReportForm(form)
|
||||
emits('createObservable', preparedModel)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical :size="16">
|
||||
<NFlex :wrap="false">
|
||||
<NInput v-model:value="searchArg"
|
||||
placeholder="Поиск пациента по ФИО"
|
||||
@keydown.enter="findPatient"
|
||||
clearable
|
||||
@clear="resetSearch"
|
||||
/>
|
||||
<NButton secondary @click="findPatient">
|
||||
<template #icon>
|
||||
<TbSearch />
|
||||
</template>
|
||||
Найти
|
||||
</NButton>
|
||||
</NFlex>
|
||||
<PatientTypeSection>
|
||||
<PatientTypeSectionItem label="Планово" :counter="patientsByGroup.planned.length">
|
||||
<PatientDataTable :data="patientsByGroup.planned" :columns="planOrEmergencyColumns" :loading="searching" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Экстренно" :counter="patientsByGroup.urgent.length">
|
||||
<PatientDataTable :data="patientsByGroup.urgent" :columns="planOrEmergencyColumns" :loading="searching" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Находятся на контроле" :counter="patientsByGroup.observables.length">
|
||||
<PatientDataTable :data="patientsByGroup.observables" :columns="observableColumns" :loading="searching" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Находятся в реанимации" :counter="patientsByGroup.reanimations.length">
|
||||
<PatientDataTable :data="patientsByGroup.reanimations" :columns="reanimationColumns" :loading="searching" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Выбывшие" :counter="patientsByGroup.discharged.length + patientsByGroup.deceased.length">
|
||||
<NTabs type="segment" animated>
|
||||
<NTabPane name="1" :tab="`Выписанные (${patientsByGroup.discharged.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.discharged" :columns="dischargedColumns" :loading="searching" />
|
||||
</NTabPane>
|
||||
<NTabPane name="2" :tab="`Умершие (${patientsByGroup.deceased.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.deceased" :columns="deceasedColumns" :loading="searching" />
|
||||
</NTabPane>
|
||||
<NTabPane name="3" :tab="`Переведенные (${patientsByGroup.transferred.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.transferred" :columns="transferredColumns" :loading="searching" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</PatientTypeSectionItem>
|
||||
</PatientTypeSection>
|
||||
</NSpace>
|
||||
|
||||
<OperationInfoModal :patient="activePatient" :operations="operationsInModal" v-model:show="showOperationsModal" />
|
||||
<ObservableModal :patient="activePatient" v-model:show="showObservableModal" @on-submit="(model) => onObservablePatient(model)" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
59
resources/js/Pages/Report/Components/HeaderWidget.vue
Normal file
59
resources/js/Pages/Report/Components/HeaderWidget.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import AppPanel from "../../../Components/AppPanel.vue";
|
||||
import {NNumberAnimation, NStatistic} from "naive-ui";
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
counter: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
isDoubleCounter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
counterSuffix: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
percent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
counterClass: {
|
||||
type: String
|
||||
},
|
||||
counterSuffixClass: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppPanel no-padding>
|
||||
<div class="flex flex-col items-center justify-center text-center py-2">
|
||||
<NStatistic :label="label">
|
||||
<template v-if="isDoubleCounter">
|
||||
<span :class="counterClass">
|
||||
<NNumberAnimation :from="0" :to="counter" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
</span>
|
||||
<span style="color: var(--n-close-icon-color)"> / </span>
|
||||
<span :class="counterSuffixClass">
|
||||
<NNumberAnimation :from="0" :to="counterSuffix" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else :class="counterClass">
|
||||
<NNumberAnimation :from="0" :to="counter" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
</span>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</AppPanel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup>
|
||||
import {NButton, NForm, NFormItem, NInput, NModal, NSpace, NText, NSpin} from "naive-ui";
|
||||
import {computed, ref, useTemplateRef, watch} from "vue";
|
||||
|
||||
const show = defineModel('show')
|
||||
const props = defineProps({
|
||||
patient: Object
|
||||
})
|
||||
const emits = defineEmits([
|
||||
'onSubmit'
|
||||
])
|
||||
|
||||
const formRef = useTemplateRef('form')
|
||||
const loading = ref(true)
|
||||
const model = ref({
|
||||
observable_reason: null
|
||||
})
|
||||
const rules = {
|
||||
observable_reason: {
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
model.value = {
|
||||
observable_reason: null
|
||||
}
|
||||
}
|
||||
|
||||
const actionText = computed(() => props.patient.in_observable ? 'Просмотр контроля' : 'Постановка на контроль')
|
||||
|
||||
const submit = () => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
emits('onSubmit', {
|
||||
...props.patient,
|
||||
...model.value
|
||||
})
|
||||
cancel()
|
||||
}
|
||||
else {
|
||||
alert(errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
const cancel = () => {
|
||||
resetForm()
|
||||
show.value = false
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
const onAfterEnter = () => {
|
||||
model.value.observable_reason = props.patient.observable?.observable_reason
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show"
|
||||
:mask-closable="false"
|
||||
@afterLeave="cancel"
|
||||
@afterEnter="onAfterEnter"
|
||||
segmented
|
||||
draggable
|
||||
class="max-w-lg relative overflow-hidden"
|
||||
preset="card"
|
||||
>
|
||||
<template #header>
|
||||
<NSpace vertical :size="1" class="text-base font-normal">
|
||||
<NText strong>
|
||||
{{ patient.full_name ?? '' }}
|
||||
</NText>
|
||||
<NText depth="3" class="text-sm">
|
||||
{{ actionText }}
|
||||
</NText>
|
||||
</NSpace>
|
||||
</template>
|
||||
<NForm ref="form" :model="model" :rules="rules">
|
||||
<NFormItem label="Опишите причину" path="observable_reason" :show-feedback="false">
|
||||
<NInput type="textarea" :rows="6" :resizable="false" v-model:value="model.observable_reason" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #action>
|
||||
<NSpace align="center" justify="end">
|
||||
<NButton type="primary" secondary @click="submit">
|
||||
Сохранить
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
<div v-if="loading" class="absolute inset-0 z-10 flex items-center justify-center" style="background-color: var(--n-color);">
|
||||
<NSpin size="small" description="Загрузка..." />
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
<script setup>
|
||||
import {NModal, NList, NListItem, NThing, NAvatar, NIcon, NDrawer, NDrawerContent,
|
||||
NText, NDivider, NForm, NFormItem, NInput, NFlex, NButton, NScrollbar, NEmpty
|
||||
} from 'naive-ui'
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { TbAlertCircle, TbPencil, TbTrashX, TbCirclePlus, TbCheck, TbX } from 'vue-icons-plus/tb'
|
||||
import {format, isValid} from "date-fns";
|
||||
|
||||
const show = defineModel('show')
|
||||
|
||||
const props = defineProps({
|
||||
report: Object,
|
||||
canSaveReport: Boolean
|
||||
})
|
||||
const emits = defineEmits([
|
||||
'onSubmit'
|
||||
])
|
||||
|
||||
const formRef = ref()
|
||||
const createDrawerShow = ref(false)
|
||||
const rules = {
|
||||
comment: {
|
||||
required: true,
|
||||
message: 'Заполните этот блок',
|
||||
trigger: 'blur'
|
||||
}
|
||||
}
|
||||
const selectedEvent = ref(null)
|
||||
const drawerCreatingMode = ref(true) // or false = editing
|
||||
const model = ref({
|
||||
unwantedEvents: []
|
||||
})
|
||||
|
||||
// Создание в сторе и открытие drawer с формой нежелательного события
|
||||
const onCreateEvent = () => {
|
||||
drawerCreatingMode.value = true
|
||||
const createDate = format(new Date(), 'Создано dd.MM.yyyy в HH:mm')
|
||||
model.value.unwantedEvents.push({
|
||||
title: `Нежелательное событие №${model.value.unwantedEvents.length + 1}`,
|
||||
comment: '',
|
||||
created_at: createDate
|
||||
})
|
||||
|
||||
const length = model.value.unwantedEvents.length
|
||||
|
||||
selectedEvent.value = model.value.unwantedEvents[length - 1]
|
||||
createDrawerShow.value = true
|
||||
}
|
||||
|
||||
const onEditEvent = (event) => {
|
||||
drawerCreatingMode.value = false
|
||||
selectedEvent.value = event
|
||||
createDrawerShow.value = true
|
||||
}
|
||||
|
||||
const hasDisableAddButton = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const onDeleteEvent = (event) => {
|
||||
const indexOfDelete = model.value.unwantedEvents.findIndex(itm => itm === event)
|
||||
|
||||
if (typeof event.unwanted_event_id !== 'undefined') {
|
||||
axios.delete(`/api/report/unwanted-event/${event.unwanted_event_id}`)
|
||||
.then(() => {
|
||||
model.value.unwantedEvents.splice(indexOfDelete, 1)
|
||||
})
|
||||
} else {
|
||||
model.value.unwantedEvents.splice(indexOfDelete, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancelDrawerEvent = (event) => {
|
||||
onDeleteEvent(event)
|
||||
createDrawerShow.value = false
|
||||
}
|
||||
|
||||
const onCreateDrawerEvent = () => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
createDrawerShow.value = false
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onBeforeLeaveModal = () => {
|
||||
selectedEvent.value = null
|
||||
drawerCreatingMode.value = true
|
||||
createDrawerShow.value = false
|
||||
emits('onSubmit', model.value.unwantedEvents)
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
resetForm()
|
||||
show.value = false
|
||||
loading.value = true
|
||||
}
|
||||
|
||||
const onAfterEnter = () => {
|
||||
model.value.unwantedEvents = props.report?.unwanted_events ?? []
|
||||
}
|
||||
|
||||
// watch(() => props.report, (newReport) => {
|
||||
// model.value.unwantedEvents = newReport?.unwanted_events ?? []
|
||||
// })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show"
|
||||
title="Нежелательные события"
|
||||
preset="card"
|
||||
:mask-closable="false"
|
||||
:close-on-esc="false"
|
||||
@afterEnter="onAfterEnter"
|
||||
@before-leave="onBeforeLeaveModal"
|
||||
class="max-w-4xl overflow-clip h-[calc(100vh-220px)]"
|
||||
>
|
||||
<template v-if="model.unwantedEvents.length">
|
||||
<NScrollbar class="max-h-[calc(100vh-282px)] pr-3">
|
||||
<NList>
|
||||
<NListItem v-for="event in model.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>
|
||||
<template v-if="canSaveReport" #action>
|
||||
<NFlex align="center">
|
||||
<NButton secondary size="small" @click="onEditEvent(event)" :disabled="hasDisableAddButton">
|
||||
<template #icon>
|
||||
<TbPencil />
|
||||
</template>
|
||||
Редактировать
|
||||
</NButton>
|
||||
<NDivider vertical />
|
||||
<NButton type="error" secondary size="small" @click="onDeleteEvent(event)" :disabled="hasDisableAddButton">
|
||||
<template #icon>
|
||||
<TbTrashX />
|
||||
</template>
|
||||
Удалить
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NThing>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NScrollbar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<NEmpty description="Нежелательных событий не найдено!">
|
||||
<template #extra>
|
||||
<NButton v-if="canSaveReport" type="primary" secondary @click="onCreateEvent()" size="small" :disabled="hasDisableAddButton">
|
||||
<template #icon>
|
||||
<TbCirclePlus />
|
||||
</template>
|
||||
Создать
|
||||
</NButton>
|
||||
</template>
|
||||
</NEmpty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="canSaveReport" #action>
|
||||
<NFlex id="modal-action" align="center" justify="space-between">
|
||||
<NButton type="primary" secondary @click="onCreateEvent()" :disabled="hasDisableAddButton">
|
||||
<template #icon>
|
||||
<TbCirclePlus />
|
||||
</template>
|
||||
Создать событие
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<NDrawer
|
||||
:show="createDrawerShow"
|
||||
placement="bottom"
|
||||
:max-height="600"
|
||||
:min-height="400"
|
||||
:default-height="400"
|
||||
resizable
|
||||
:trap-focus="false"
|
||||
:block-scroll="false"
|
||||
:mask-closable="false"
|
||||
to="#modal-action"
|
||||
>
|
||||
<NDrawerContent>
|
||||
<template #header>
|
||||
<template v-if="drawerCreatingMode">Создание события</template>
|
||||
<template v-else>Редактирование события</template>
|
||||
</template>
|
||||
<NForm ref="formRef" :model="selectedEvent" :rules="rules">
|
||||
<NFormItem :show-label="false" path="comment">
|
||||
<NInput type="textarea" :rows="8" v-model:value="selectedEvent.comment" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NFlex align="center">
|
||||
<NButton v-if="drawerCreatingMode" type="error" secondary @click="onCancelDrawerEvent(selectedEvent)">
|
||||
<template #icon>
|
||||
<TbX />
|
||||
</template>
|
||||
Отменить создание
|
||||
</NButton>
|
||||
<NDivider v-if="drawerCreatingMode" vertical />
|
||||
<NButton type="primary" @click="onCreateDrawerEvent">
|
||||
<template #icon>
|
||||
<TbCheck />
|
||||
</template>
|
||||
<template v-if="drawerCreatingMode">Создать</template>
|
||||
<template v-else>Сохранить</template>
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
96
resources/js/Pages/Report/Components/NursePatientsPane.vue
Normal file
96
resources/js/Pages/Report/Components/NursePatientsPane.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import PatientTypeSection from "./PatientSection.vue";
|
||||
import PatientDataTable from "./PatientDataTable.vue";
|
||||
import {NTabPane, NTabs} from "naive-ui";
|
||||
import PatientTypeSectionItem from "./PatientSectionItem.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {usePatientColumns} from "../../../Composables/usePatientColumns.js";
|
||||
import {router} from "@inertiajs/vue3";
|
||||
import OperationInfoModal from "./OperationInfoModal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
patients: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
}
|
||||
})
|
||||
|
||||
const operationsInModal = ref(null)
|
||||
const showOperationsModal = ref(false)
|
||||
|
||||
const patientsByGroup = computed(() => {
|
||||
const groups = {
|
||||
urgent: [],
|
||||
planned: [],
|
||||
deceased: [],
|
||||
in_department: [],
|
||||
recipient: [],
|
||||
discharged: [],
|
||||
transferred: [],
|
||||
reanimations: [],
|
||||
observables: []
|
||||
}
|
||||
|
||||
if (!props.patients.hasOwnProperty('data')) return groups
|
||||
|
||||
for (const p of props.patients.data) {
|
||||
// Группировка по срочности
|
||||
if (p.patient_urgency === 'urgent') groups.urgent.push(p)
|
||||
else if (p.patient_urgency === 'planned') groups.planned.push(p)
|
||||
|
||||
// Группировка по реанимации
|
||||
if (p.in_reanimation === true) groups.reanimations.push(p)
|
||||
// Группировка по наблюдению
|
||||
if (p.in_observable === true) groups.observables.push(p)
|
||||
|
||||
// Группировка по статусу (дублирование нужно, если один пациент может быть в двух таблицах)
|
||||
if (groups.hasOwnProperty(p.patient_status)) {
|
||||
groups[p.patient_status].push(p)
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
})
|
||||
|
||||
const {
|
||||
planOrEmergencyColumns, observableColumns, reanimationColumns, dischargedColumns,
|
||||
deceasedColumns, transferredColumns,
|
||||
} = usePatientColumns({
|
||||
onAddObservable: (row) => {
|
||||
},
|
||||
onShowOperationModal: (operations) => {
|
||||
operationsInModal.value = operations
|
||||
showOperationsModal.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PatientTypeSection>
|
||||
<PatientTypeSectionItem label="Планово" :counter="patientsByGroup.planned.length">
|
||||
<PatientDataTable :data="patientsByGroup.planned" :columns="planOrEmergencyColumns" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Экстренно" :counter="patientsByGroup.urgent.length">
|
||||
<PatientDataTable :data="patientsByGroup.urgent" :columns="planOrEmergencyColumns" />
|
||||
</PatientTypeSectionItem>
|
||||
<PatientTypeSectionItem label="Выбывшие" :counter="patientsByGroup.discharged.length + patientsByGroup.deceased.length">
|
||||
<NTabs type="segment" animated>
|
||||
<NTabPane name="1" :tab="`Выписанные (${patientsByGroup.discharged.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.discharged" :columns="dischargedColumns" />
|
||||
</NTabPane>
|
||||
<NTabPane name="2" :tab="`Умершие (${patientsByGroup.deceased.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.deceased" :columns="deceasedColumns" />
|
||||
</NTabPane>
|
||||
<NTabPane name="3" :tab="`Переведенные (${patientsByGroup.transferred.length})`">
|
||||
<PatientDataTable :data="patientsByGroup.transferred" :columns="transferredColumns" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</PatientTypeSectionItem>
|
||||
</PatientTypeSection>
|
||||
|
||||
<OperationInfoModal :operations="operationsInModal" v-model:show="showOperationsModal" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup>
|
||||
import {NModal, NThing, NTag, NButton, NSpace, NDrawer, NDrawerContent, NInput} from "naive-ui";
|
||||
import {NModal, NThing, NTag, NButton, NSpace, NDrawer, NDrawerContent, NInput, NText} from "naive-ui";
|
||||
import {ref, watch} from "vue";
|
||||
import {TbClockCheck, TbCalendar, TbTag} from 'vue-icons-plus/tb'
|
||||
import {format} from "date-fns";
|
||||
import AppPanel from "../../../Components/AppPanel.vue";
|
||||
import UrgencyBadge from "../../../Components/UrgencyBadge.vue";
|
||||
import OperationUrgencyTag from "./Tags/OperationUrgencyTag.vue";
|
||||
|
||||
const props = defineProps({
|
||||
operations: Array
|
||||
operations: Array,
|
||||
patient: Object
|
||||
})
|
||||
|
||||
const show = defineModel('show')
|
||||
@@ -21,19 +24,28 @@ const onShowInfoDrawer = (operation) => {
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show"
|
||||
title="Операции"
|
||||
:mask-closable="false"
|
||||
:segmented="{ content: true }"
|
||||
segmented
|
||||
class="max-w-xl overflow-clip h-[400px]"
|
||||
draggable
|
||||
content-scrollable
|
||||
preset="card"
|
||||
id="modal-operation"
|
||||
>
|
||||
<template #header>
|
||||
<NSpace vertical :size="1" class="text-base font-normal">
|
||||
<NText strong>
|
||||
{{ patient.full_name ?? '' }}
|
||||
</NText>
|
||||
<NText depth="3" class="text-sm">
|
||||
Проведенные операции
|
||||
</NText>
|
||||
</NSpace>
|
||||
</template>
|
||||
<div class="space-y-4">
|
||||
<NThing v-for="operation in operations">
|
||||
<template #header>
|
||||
Операция №
|
||||
Операция №{{operation.number}}
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NTag size="small" type="info" round :bordered="false">
|
||||
@@ -45,18 +57,19 @@ const onShowInfoDrawer = (operation) => {
|
||||
</template>
|
||||
<template #description>
|
||||
<NSpace align="center" size="small">
|
||||
<NTag type="success" round :bordered="false">
|
||||
<NTag type="success" round :bordered="false" size="small">
|
||||
<template #icon>
|
||||
<TbCalendar size="18" />
|
||||
<TbCalendar size="16" />
|
||||
</template>
|
||||
{{format(operation.start_date, 'dd.MM.yyyy')}}
|
||||
</NTag>
|
||||
<NTag type="success" round :bordered="false">
|
||||
<NTag type="success" round :bordered="false" size="small">
|
||||
<template #icon>
|
||||
<TbTag size="18" />
|
||||
<TbTag size="16" />
|
||||
</template>
|
||||
{{operation.code_service}}
|
||||
</NTag>
|
||||
<OperationUrgencyTag :urgency-id="operation.urgent_status" size="small" />
|
||||
</NSpace>
|
||||
</template>
|
||||
{{operation.name_service}}
|
||||
@@ -70,7 +83,7 @@ const onShowInfoDrawer = (operation) => {
|
||||
<NDrawer v-model:show="showInfoDrawer" width="100%" height="100%" to="#modal-operation" placement="bottom">
|
||||
<NDrawerContent closable>
|
||||
<template #header>
|
||||
Операция №
|
||||
Операция №{{showedOperation.number}}
|
||||
</template>
|
||||
<AppPanel no-padding header="Описание" class="h-full!">
|
||||
<NInput type="textarea" :resizable="false" class="h-full!" readonly v-model:value="showedOperation.description" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import {NDataTable, NSpace, NInput, NButton, NFlex} from "naive-ui";
|
||||
import {TbSearch} from 'vue-icons-plus/tb'
|
||||
import {computed, h, ref} from "vue";
|
||||
import {NDataTable} from "naive-ui";
|
||||
import {computed, h, ref, watch} from "vue";
|
||||
import IndexColumn from "./DataTableColumns/IndexColumn.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -19,6 +18,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const patients = ref([...props.data])
|
||||
const loading = ref(false)
|
||||
const tableColumns = computed(() => {
|
||||
const baseColumns = [...props.columns]
|
||||
|
||||
@@ -36,46 +36,39 @@ const tableColumns = computed(() => {
|
||||
|
||||
return baseColumns
|
||||
})
|
||||
const searchArg = ref(null)
|
||||
|
||||
const findPatient = (arg) => {
|
||||
// TODO: сделать поиск пациента через БДц
|
||||
}
|
||||
|
||||
const rowProps = (row) => {
|
||||
const style = []
|
||||
|
||||
if (row.admitted_today) {
|
||||
style.push('--n-merged-td-color: #047857')
|
||||
} else if (row.nurse_changes?.length) {
|
||||
style.push('--n-merged-td-color: rgba(217, 119, 6, 0.15)')
|
||||
}
|
||||
|
||||
return {
|
||||
style: style,
|
||||
style: style.join('; '),
|
||||
title: row.nurse_changes?.length
|
||||
? `Медсестра изменила: ${row.nurse_changes_labels.join(', ')}`
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.data, (newData) => {
|
||||
patients.value = newData
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NFlex :wrap="false">
|
||||
<NInput v-model:value="searchArg" placeholder="Поиск пациента" @input="value => findPatient(value)" />
|
||||
<NButton secondary @click="findPatient(searchArg)">
|
||||
<template #icon>
|
||||
<TbSearch />
|
||||
</template>
|
||||
Найти
|
||||
</NButton>
|
||||
</NFlex>
|
||||
<NDataTable :columns="tableColumns"
|
||||
:data="patients"
|
||||
table-layout="fixed"
|
||||
max-height="234"
|
||||
min-height="234"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:row-props="rowProps"
|
||||
/>
|
||||
</NSpace>
|
||||
<NDataTable :columns="tableColumns"
|
||||
:data="patients"
|
||||
table-layout="fixed"
|
||||
max-height="234"
|
||||
min-height="234"
|
||||
:loading="loading"
|
||||
size="small"
|
||||
:row-props="rowProps"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,7 +11,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const counterText = computed(() => props.counter ? `(${props.counter})` : '')
|
||||
const counterText = computed(() => props.counter ? `(${props.counter})` : '(0)')
|
||||
|
||||
const header = computed(() => `${props.label} ${counterText.value}`)
|
||||
</script>
|
||||
|
||||
@@ -42,8 +42,6 @@ const handleCollapseItemDragEnter = (e, itemName) => {
|
||||
const handleItemDropped = (event) => {
|
||||
const { item, fromStatus, toStatus } = event
|
||||
|
||||
console.log(event)
|
||||
|
||||
// Добавляем в целевую таблицу
|
||||
if (toStatus && patientsData.value[toStatus]) {
|
||||
// Проверяем, нет ли уже такого элемента
|
||||
|
||||
@@ -3,7 +3,22 @@ import AppPanel from "../../../Components/AppPanel.vue";
|
||||
import {NNumberAnimation, NStatistic} from "naive-ui";
|
||||
|
||||
const props = defineProps({
|
||||
to: Number,
|
||||
counter: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
isDoubleCounter: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
counterSuffix: {
|
||||
type: [String, Number],
|
||||
default: null
|
||||
},
|
||||
percent: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -12,10 +27,18 @@ const props = defineProps({
|
||||
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 class="text-center">
|
||||
<NNumberAnimation :from="0" :to="to" />
|
||||
<template #label>
|
||||
<slot />
|
||||
</template>
|
||||
<template v-if="isDoubleCounter">
|
||||
<NNumberAnimation :from="0" :to="counter" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
<span style="color: var(--n-close-icon-color)"> / </span>
|
||||
<NNumberAnimation :from="0" :to="counterSuffix" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
</template>
|
||||
<NNumberAnimation v-else :from="0" :to="counter" />
|
||||
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
|
||||
</NStatistic>
|
||||
</div>
|
||||
</AppPanel>
|
||||
|
||||
@@ -5,6 +5,22 @@ import {computed, ref, watch} from "vue";
|
||||
import {router, Link} from "@inertiajs/vue3";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
const show = defineModel('show')
|
||||
|
||||
const props = defineProps({
|
||||
targetPath: {
|
||||
type: String,
|
||||
default: '/report'
|
||||
},
|
||||
submitLabel: {
|
||||
type: String,
|
||||
default: 'Перейти к заполнению сводной'
|
||||
},
|
||||
withExistsCheck: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const reportStore = useReportStore()
|
||||
const formRef = ref()
|
||||
|
||||
@@ -44,7 +60,7 @@ const fetchDepartments = () => {
|
||||
}
|
||||
|
||||
const checkReportExists = async (userId, departmentId) => {
|
||||
if (!userId || !departmentId) {
|
||||
if (!props.withExistsCheck || !userId || !departmentId) {
|
||||
reportExists.value = false;
|
||||
existingReportId.value = null;
|
||||
return;
|
||||
@@ -64,7 +80,6 @@ const checkReportExists = async (userId, departmentId) => {
|
||||
console.error('Ошибка при проверке отчета:', error);
|
||||
reportExists.value = false;
|
||||
existingReportId.value = null;
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +139,7 @@ const onSubmit = (e) => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
if (reportExists.value) return
|
||||
router.visit(`/report`, {
|
||||
router.visit(props.targetPath, {
|
||||
data: {
|
||||
userId: reportStore.reportInfo.userId,
|
||||
departmentId: reportStore.reportInfo.departmentId
|
||||
@@ -166,9 +181,9 @@ const onAfterLeave = () => {
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<NAlert v-if="reportExists" type="warning">
|
||||
<NAlert v-if="reportExists && withExistsCheck" type="warning">
|
||||
Сводная уже создана.
|
||||
<NButton :tag="Link" text :href="`/report?userId=${reportStore.reportInfo.userId}&departmentId=${reportStore.reportInfo.departmentId}`">
|
||||
<NButton :tag="Link" text :href="`${targetPath}?userId=${reportStore.reportInfo.userId}&departmentId=${reportStore.reportInfo.departmentId}`">
|
||||
Перейти
|
||||
</NButton>
|
||||
</NAlert>
|
||||
@@ -177,8 +192,8 @@ const onAfterLeave = () => {
|
||||
type="primary"
|
||||
block
|
||||
@click="onSubmit"
|
||||
:disabled="reportExists">
|
||||
Перейти к заполнению сводной
|
||||
:disabled="withExistsCheck && reportExists">
|
||||
{{ submitLabel }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import {NTag} from "naive-ui"
|
||||
import {computed} from "vue";
|
||||
const props = defineProps({
|
||||
urgencyId: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
})
|
||||
|
||||
const urgentIds = [4, 5]
|
||||
const plannedIds = [6]
|
||||
|
||||
const typeForUrgency = computed(() => plannedIds.includes(props.urgencyId) ? 'success' : 'error')
|
||||
const textForUrgency = computed(() => plannedIds.includes(props.urgencyId) ? 'Планово' : 'Экстренно')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NTag :type="typeForUrgency" round :bordered="false" size="small">
|
||||
{{ textForUrgency }}
|
||||
</NTag>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,6 +8,9 @@ const open = defineModel('open')
|
||||
import { TbAlertCircle, TbPencil, TbTrashX, TbCirclePlus, TbCheck, TbX } from 'vue-icons-plus/tb'
|
||||
import {format, isValid} from "date-fns";
|
||||
|
||||
const props = defineProps({
|
||||
canSaveReport: Boolean
|
||||
})
|
||||
const reportStore = useReportStore()
|
||||
const formRef = ref()
|
||||
const createDrawerShow = ref(false)
|
||||
@@ -68,9 +71,7 @@ const onCancelDrawerEvent = (event) => {
|
||||
const onCreateDrawerEvent = () => {
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors) {
|
||||
console.log(createDrawerShow.value)
|
||||
createDrawerShow.value = false
|
||||
console.log(createDrawerShow.value)
|
||||
}
|
||||
else {
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import HeaderWidget from "../HeaderWidget.vue"
|
||||
import {computed} from "vue";
|
||||
const props = defineProps({
|
||||
counter: Number
|
||||
})
|
||||
|
||||
const counterClass = computed(() => {
|
||||
const value = typeof props.counter === 'number' ? props.counter : 0
|
||||
|
||||
if (value < 60) return 'counter-success'
|
||||
if (value < 100) return 'counter-warning'
|
||||
return 'counter-danger'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeaderWidget label="Загруженность"
|
||||
:counter="counter"
|
||||
:counter-class="counterClass"
|
||||
percent
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.counter-success) {
|
||||
color: var(--n-color-target) !important;
|
||||
}
|
||||
|
||||
:deep(.counter-warning) {
|
||||
color: var(--n-feedback-text-color-warning) !important;
|
||||
}
|
||||
|
||||
:deep(.counter-danger) {
|
||||
color: var(--n-asterisk-color) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import {NModal, NSpace} from 'naive-ui'
|
||||
import ReportWidget from "../ReportWidget.vue";
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
operations: Array,
|
||||
planned: Number,
|
||||
urgent: Number,
|
||||
})
|
||||
|
||||
const showOperationsModal = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ReportWidget :counter="urgent" is-double-counter :counter-suffix="planned">
|
||||
<NSpace vertical :size="1">
|
||||
<div>Операций</div>
|
||||
<div>Э / П</div>
|
||||
</NSpace>
|
||||
</ReportWidget>
|
||||
|
||||
<NModal v-model:show="showOperationsModal" preset="card" segmented class="max-w-screen md:max-w-3xl lg:max-w-6xl">
|
||||
<template #header>
|
||||
Кому провели операции на сегодня
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
import HeaderWidget from "../HeaderWidget.vue"
|
||||
import {computed} from "vue";
|
||||
const props = defineProps({
|
||||
dutyCurrent: Number,
|
||||
nurseCurrent: Number,
|
||||
beds: Number,
|
||||
})
|
||||
|
||||
const getColorClassByAbsolute = (current) => {
|
||||
const freeBeds = props.beds - current
|
||||
|
||||
if (freeBeds >= 5) return 'counter-success' // Свободно 5+ коек
|
||||
if (freeBeds >= 2) return 'counter-warning' // Свободно 2-4 койки
|
||||
// if (freeBeds >= 0) return 'counter-warning-high' // Свободно 0-1 койка
|
||||
return 'counter-danger' // Переполнение
|
||||
}
|
||||
|
||||
const dutyClass = computed(() => {
|
||||
const dutyValue = typeof props.dutyCurrent === 'number' ? props.dutyCurrent : 0
|
||||
return getColorClassByAbsolute(dutyValue)
|
||||
})
|
||||
|
||||
const nurseClass = computed(() => {
|
||||
const nurseValue = typeof props.nurseCurrent === 'number' ? props.nurseCurrent : 0
|
||||
return getColorClassByAbsolute(nurseValue)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HeaderWidget label="Состоит"
|
||||
is-double-counter
|
||||
:counter="dutyCurrent"
|
||||
:counter-suffix="nurseCurrent"
|
||||
:counter-class="dutyClass"
|
||||
:counter-suffix-class="nurseClass"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.counter-success) {
|
||||
color: var(--n-color-target) !important;
|
||||
}
|
||||
|
||||
:deep(.counter-warning) {
|
||||
color: var(--n-feedback-text-color-warning) !important;
|
||||
}
|
||||
|
||||
:deep(.counter-danger) {
|
||||
color: var(--n-asterisk-color) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user