Добавил уведомление о создании отчета

This commit is contained in:
brusnitsyn
2026-06-23 17:28:52 +09:00
parent 8e6bbc5f91
commit 6942b0c091
9 changed files with 121 additions and 42 deletions

View File

@@ -10,8 +10,16 @@ export function useNotification() {
return null return null
} }
const success = () => { const success = (title, description = '', options = {}) => {
return showNotification(
{
title,
description,
meta: options.meta || new Date().toLocaleDateString(),
...options,
type: 'success'
}
)
} }
const errorApi = (data, content = '', options = {}) => { const errorApi = (data, content = '', options = {}) => {
@@ -36,6 +44,7 @@ export function useNotification() {
} }
return { return {
errorApi errorApi,
success
} }
} }

View File

@@ -229,7 +229,6 @@ watch([dateModel, departmentModel], () => {
<NDropdown trigger="click" :options="exportMenu" @select="download"> <NDropdown trigger="click" :options="exportMenu" @select="download">
<NButton> <NButton>
<template #icon><TbDownload /></template> <template #icon><TbDownload /></template>
Скачать
</NButton> </NButton>
</NDropdown> </NDropdown>
<NButton v-if="canManage" @click="save"> <NButton v-if="canManage" @click="save">
@@ -330,7 +329,7 @@ watch([dateModel, departmentModel], () => {
flex-shrink: 0; flex-shrink: 0;
position: sticky; position: sticky;
top: 12px; top: 12px;
border: 1px solid var(--n-border-color, rgba(255,255,255,.1)); border: 1px solid var(--border-color, rgba(255,255,255,.1));
border-radius: 14px; border-radius: 14px;
background: var(--card-color); background: var(--card-color);
padding: 14px; padding: 14px;
@@ -356,9 +355,9 @@ watch([dateModel, departmentModel], () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid var(--n-border-color, rgba(255,255,255,.12)); border: 1px solid var(--border-color, rgba(255,255,255,.12));
border-radius: 8px; border-radius: 8px;
background: var(--n-card-color); background: var(--card-color);
cursor: pointer; cursor: pointer;
} }
.slide-enter-active, .slide-leave-active { transition: opacity .15s, transform .15s; } .slide-enter-active, .slide-leave-active { transition: opacity .15s, transform .15s; }

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { NText } from 'naive-ui' import { NText, NEl } from 'naive-ui'
import { TbPlus } from 'vue-icons-plus/tb' import { TbPlus } from 'vue-icons-plus/tb'
defineProps({ defineProps({
@@ -8,7 +8,7 @@ defineProps({
</script> </script>
<template> <template>
<div class="preset-card"> <NEl class="preset-card">
<div v-if="preset.key === 'blank'" class="preset-blank"> <div v-if="preset.key === 'blank'" class="preset-blank">
<TbPlus :size="26" /> <TbPlus :size="26" />
</div> </div>
@@ -17,7 +17,7 @@ defineProps({
</div> </div>
<NText class="preset-title">{{ preset.label }}</NText> <NText class="preset-title">{{ preset.label }}</NText>
<NText depth="3" class="preset-desc">{{ preset.description }}</NText> <NText depth="3" class="preset-desc">{{ preset.description }}</NText>
</div> </NEl>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { import {
NButton, NText, NIcon, NSelect, NScrollbar, NGrid, NGi, NAlert, NRadio, NEmpty, NButton, NText, NIcon, NSelect, NScrollbar, NGrid, NGi, NAlert, NRadio, NEmpty, NEl
} from 'naive-ui' } from 'naive-ui'
import { import {
TbX, TbChevronRight, TbCalendar, TbTrendingUp, TbLayoutGrid, TbDatabase, TbX, TbChevronRight, TbCalendar, TbTrendingUp, TbLayoutGrid, TbDatabase,
@@ -60,7 +60,7 @@ const filterOptions = (def) => (def.options ?? [])
</script> </script>
<template> <template>
<div class="settings"> <NEl class="settings">
<!-- ROOT --> <!-- ROOT -->
<template v-if="view === 'root'"> <template v-if="view === 'root'">
<div class="head"> <div class="head">
@@ -242,11 +242,11 @@ const filterOptions = (def) => (def.options ?? [])
/> />
</div> </div>
</template> </template>
</div> </NEl>
</template> </template>
<style scoped> <style scoped>
.settings { padding: 4px 2px; background: var(--n-) } .settings { padding: 4px 2px; }
.head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.head-title { font-size: 18px; font-weight: 600; } .head-title { font-size: 18px; font-weight: 600; }
.sub-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } .sub-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
@@ -257,15 +257,15 @@ const filterOptions = (def) => (def.options ?? [])
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid var(--n-border-color, rgba(255,255,255,.1)); border: 1px solid var(--border-color, rgba(255,255,255,.1));
border-radius: 12px; border-radius: 12px;
margin-bottom: 8px; margin-bottom: 8px;
cursor: pointer; cursor: pointer;
background: var(--n-card-color); background: var(--card-color);
transition: border-color .15s; transition: border-color .15s;
} }
.row:hover { border-color: var(--primary-color); } .row:hover { border-color: var(--primary-color); }
.row.static, .row.static:hover { cursor: default; border-color: var(--n-border-color, rgba(255,255,255,.1)); } .row.static, .row.static:hover { cursor: default; border-color: var(--border-color, rgba(255,255,255,.1)); }
.row.disabled { opacity: .5; pointer-events: none; } .row.disabled { opacity: .5; pointer-events: none; }
.row-icon { color: var(--primary-color); } .row-icon { color: var(--primary-color); }
.row-label { flex: 1; font-weight: 500; } .row-label { flex: 1; font-weight: 500; }
@@ -293,7 +293,7 @@ const filterOptions = (def) => (def.options ?? [])
font-size: 13px; font-size: 13px;
color: var(--n-text-color-2); color: var(--n-text-color-2);
} }
.seg-btn.active { background: var(--n-card-color); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,.1); } .seg-btn.active { background: var(--card-color); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
.data-list { display: flex; flex-direction: column; gap: 8px; } .data-list { display: flex; flex-direction: column; gap: 8px; }
.data-card { .data-card {
display: flex; display: flex;
@@ -301,7 +301,7 @@ const filterOptions = (def) => (def.options ?? [])
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
padding: 12px 14px; padding: 12px 14px;
border: 1px solid var(--n-border-color, rgba(255,255,255,.1)); border: 1px solid var(--border-color, rgba(255,255,255,.1));
border-radius: 12px; border-radius: 12px;
cursor: pointer; cursor: pointer;
} }

View File

@@ -32,16 +32,17 @@ const pick = (preset) => {
:show="show" :show="show"
preset="card" preset="card"
title="Шаблоны для отчёта" title="Шаблоны для отчёта"
style="width: 860px; max-width: 94vw;" class="max-w-[860px] h-[580px] relative"
class="h-[580px]" :content-scrollable="true"
@update:show="emit('update:show', $event)" @update:show="emit('update:show', $event)"
> >
<template #header-extra> <template #header-extra>
<NText depth="3" style="font-size: 13px;">Создавайте отчёт по шаблонам</NText> <NText depth="3" style="font-size: 13px;">Создавайте отчёт по шаблонам</NText>
</template> </template>
<div class="picker"> <div class="picker relative flex gap-2 items-start">
<div class="picker-side"> <!-- Боковая панель с sticky -->
<div class="picker-side sticky top-0 shrink-0 align-start">
<NInput v-model:value="search" placeholder="Поиск" clearable size="small"> <NInput v-model:value="search" placeholder="Поиск" clearable size="small">
<template #prefix><TbSearch :size="14" /></template> <template #prefix><TbSearch :size="14" /></template>
</NInput> </NInput>
@@ -58,13 +59,14 @@ const pick = (preset) => {
</div> </div>
</div> </div>
<NScrollbar style="max-height: 60vh;" class="picker-main"> <!-- Контент справа -->
<div style="flex: 1; min-width: 0;">
<NGrid responsive="screen" cols="2 s:3" :x-gap="12" :y-gap="12" class="pt-0.5"> <NGrid responsive="screen" cols="2 s:3" :x-gap="12" :y-gap="12" class="pt-0.5">
<NGi v-for="preset in filtered" :key="preset.key"> <NGi v-for="preset in filtered" :key="preset.key">
<PresetCard :preset="preset" @click="pick(preset)" /> <PresetCard :preset="preset" @click="pick(preset)" />
</NGi> </NGi>
</NGrid> </NGrid>
</NScrollbar> </div>
</div> </div>
</NModal> </NModal>
</template> </template>

View File

@@ -4,6 +4,7 @@ import { router, Link } from '@inertiajs/vue3'
import { import {
NButton, NText, NDataTable, NEmpty, NDropdown, NIcon, NScrollbar, NButton, NText, NDataTable, NEmpty, NDropdown, NIcon, NScrollbar,
NTag, useDialog, useMessage, NTag, useDialog, useMessage,
NEl,
} from 'naive-ui' } from 'naive-ui'
import { TbReportMedical, TbPlus, TbDotsVertical, TbLayoutGrid } from 'vue-icons-plus/tb' import { TbReportMedical, TbPlus, TbDotsVertical, TbLayoutGrid } from 'vue-icons-plus/tb'
import AppLayout from '../../Layouts/AppLayout.vue' import AppLayout from '../../Layouts/AppLayout.vue'
@@ -54,7 +55,7 @@ const remove = (row) => {
const rowMenu = (row) => [ const rowMenu = (row) => [
{ key: 'open', label: 'Открыть' }, { key: 'open', label: 'Открыть' },
{ key: 'duplicate', label: 'Дублировать' }, { key: 'duplicate', label: 'Дублировать' },
{ key: 'delete', label: 'Удалить', props: { style: 'color: var(--error-color);' } }, { key: 'delete', label: 'Удалить' },
] ]
const onMenuSelect = (key, row) => { const onMenuSelect = (key, row) => {
@@ -85,7 +86,8 @@ const columns = computed(() => [
options: rowMenu(row), options: rowMenu(row),
onSelect: (key) => onMenuSelect(key, row), onSelect: (key) => onMenuSelect(key, row),
}, () => h(NButton, { }, () => h(NButton, {
text: true, round: true,
size: 'small',
onClick: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(),
}, () => h(NIcon, null, () => h(TbDotsVertical)))) : null, }, () => h(NIcon, null, () => h(TbDotsVertical)))) : null,
}, },
@@ -114,10 +116,10 @@ const columns = computed(() => [
</template> </template>
<NScrollbar x-scrollable class="pb-3"> <NScrollbar x-scrollable class="pb-3">
<div class="create-row"> <div class="create-row">
<div class="create-card" @click="openPreset({ key: 'blank' })"> <NEl class="create-card" @click="openPreset({ key: 'blank' })">
<div class="blank-tile"><NIcon :size="28"><TbPlus /></NIcon></div> <div class="blank-tile"><NIcon :size="28"><TbPlus /></NIcon></div>
<NText class="blank-title">Отчёт с нуля</NText> <NText class="blank-title">Отчёт с нуля</NText>
</div> </NEl>
<div v-for="preset in featured" :key="preset.key" class="create-card" @click="openPreset(preset)"> <div v-for="preset in featured" :key="preset.key" class="create-card" @click="openPreset(preset)">
<PresetCard :preset="preset" /> <PresetCard :preset="preset" />
</div> </div>

View File

@@ -1,6 +1,7 @@
<script setup> <script setup>
import AppLayout from "../../../Layouts/AppLayout.vue"; import AppLayout from "../../../Layouts/AppLayout.vue";
import {NFlex, NTag, NDataTable, NButton, NTabs, NTabPane, NSpace} from 'naive-ui' import {NFlex, NTag, NDataTable, NButton, NTabs, NTabPane, NSpace} from 'naive-ui'
import {useNotification} from "../../../Composables/useNotification.js"
import AppContainer from "../../../Components/AppContainer.vue"; import AppContainer from "../../../Components/AppContainer.vue";
import AppPanel from "../../../Components/AppPanel.vue"; import AppPanel from "../../../Components/AppPanel.vue";
import ShiftPickerQuery from "../../../Components/ShiftPickerQuery.vue"; import ShiftPickerQuery from "../../../Components/ShiftPickerQuery.vue";
@@ -163,13 +164,46 @@ const onClickDeleteButton = async (historyId) => {
} }
} }
const {success} = useNotification()
const notifyReportSaved = () => {
const notification = success('Сохранено', 'Отчет успешно сохранен', {
meta: '',
duration: 5000,
action: () => h(
NButton,
{
text: true,
type: 'success',
onClick: () => {
notification.destroy()
router.visit('/')
}
},
{default: () => 'Вернуться на главную страницу'}
)
})
}
const submit = () => { const submit = () => {
router.post('/nurse/report/save', { const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const url = new URL(`${window.location.origin}/nurse/report/save`)
const startAt = params.get('startAt')
const endAt = params.get('endAt')
if (startAt && startAt !== 'null') url.searchParams.append('startAt', startAt)
if (endAt && endAt !== 'null') url.searchParams.append('endAt', endAt)
loading.value = true
router.post(url.toString(), {
userId: props.selectedUserId, userId: props.selectedUserId,
departmentId: props.selectedDepartmentId, departmentId: props.selectedDepartmentId,
}, { }, {
onSuccess: () => { onSuccess: () => {
alert('Сохранено') notifyReportSaved()
},
onFinish: () => {
loading.value = false
} }
}) })
} }

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import AppPanel from "../../../Components/AppPanel.vue"; import AppPanel from "../../../Components/AppPanel.vue";
import {NNumberAnimation, NStatistic} from "naive-ui"; import {NNumberAnimation, NStatistic, NTooltip} from "naive-ui";
const props = defineProps({ const props = defineProps({
label: String, label: String,
counter: { counter: {
@@ -35,15 +35,27 @@ const props = defineProps({
<div class="flex flex-col items-center justify-center text-center py-2"> <div class="flex flex-col items-center justify-center text-center py-2">
<NStatistic :label="label"> <NStatistic :label="label">
<template v-if="isDoubleCounter"> <template v-if="isDoubleCounter">
<span :class="counterClass"> <NTooltip>
<NNumberAnimation :from="0" :to="counter" /> <template #trigger>
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span> <span :class="counterClass">
</span> <NNumberAnimation :from="0" :to="counter" />
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
</span>
</template>
Данные из МИС
</NTooltip>
<span style="color: var(--n-close-icon-color)"> / </span> <span style="color: var(--n-close-icon-color)"> / </span>
<span :class="counterSuffixClass"> <NTooltip>
<NNumberAnimation :from="0" :to="counterSuffix" /> <template #trigger>
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span> <span :class="counterSuffixClass">
</span> <NNumberAnimation :from="0" :to="counterSuffix" />
<span v-if="percent" style="color: var(--n-close-icon-color)">%</span>
</span>
</template>
Данные из журнала пациентов
</NTooltip>
</template> </template>
<span v-else :class="counterClass"> <span v-else :class="counterClass">
<NNumberAnimation :from="0" :to="counter" /> <NNumberAnimation :from="0" :to="counter" />

View File

@@ -1,7 +1,8 @@
<script setup> <script setup>
import AppLayout from "../../Layouts/AppLayout.vue"; import AppLayout from "../../Layouts/AppLayout.vue";
import {useReportStore} from "../../Stores/report.js"; import {useReportStore} from "../../Stores/report.js";
import {onMounted, ref, watch, provide, computed} from "vue"; import {onMounted, ref, watch, provide, computed, h} from "vue";
import {useNotification} from "../../Composables/useNotification.js";
import {useAuthStore} from "../../Stores/auth.js"; import {useAuthStore} from "../../Stores/auth.js";
import { import {
NFormItem, NFormItem,
@@ -88,6 +89,26 @@ const props = defineProps({
const reportStore = useReportStore() const reportStore = useReportStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const userDepartment = authStore.userDepartment const userDepartment = authStore.userDepartment
const {success} = useNotification()
const notifyReportSaved = () => {
const notification = success('Сохранено', 'Отчет успешно сохранен', {
meta: '',
duration: 5000,
action: () => h(
NButton,
{
text: true,
type: 'success',
onClick: () => {
notification.destroy()
router.visit('/')
}
},
{default: () => 'Вернуться на главную страницу'}
)
})
}
const patientCollection = ref({...props.patients}) const patientCollection = ref({...props.patients})
const nursePatientCollection = ref({...props.nursePatients}) const nursePatientCollection = ref({...props.nursePatients})
@@ -142,7 +163,7 @@ const submit = () => {
departmentId: props.selectedDepartmentId, departmentId: props.selectedDepartmentId,
}, { }, {
onSuccess: () => { onSuccess: () => {
alert('Сохранено') notifyReportSaved()
} }
}) })
} }