Работа над интерактивным обучением

This commit is contained in:
brusnitsyn
2026-07-14 17:30:33 +09:00
parent 7ec32eb097
commit 082f2b8b01
15 changed files with 609 additions and 19 deletions

View File

@@ -16,7 +16,8 @@
"maatwebsite/excel": "^3.1",
"pusher/pusher-php-server": "^7.2",
"spatie/laravel-permission": "^7.4",
"spatie/laravel-sluggable": "^4.0"
"spatie/laravel-sluggable": "^4.0",
"tightenco/ziggy": "^2.6"
},
"require-dev": {
"fakerphp/faker": "^1.23",

76
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a82c5f712051f2c26c196e50b7057bf4",
"content-hash": "e8468d46ea66c9a39170e5847ddf2bac",
"packages": [
{
"name": "barryvdh/laravel-dompdf",
@@ -8181,6 +8181,76 @@
],
"time": "2026-02-04T18:08:13+00:00"
},
{
"name": "tightenco/ziggy",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "14c5744f155182188419f7729d96e0ed7225e73b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/14c5744f155182188419f7729d96e0ed7225e73b",
"reference": "14c5744f155182188419f7729d96e0ed7225e73b",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": ">=9.0",
"php": ">=8.1"
},
"require-dev": {
"laravel/folio": "^1.1",
"orchestra/testbench": "^8.0 || ^9.0 || ^10.0 || ^11.0",
"pestphp/pest": "^2.0 || ^3.0 || ^4.0",
"pestphp/pest-plugin-laravel": "^2.0 || ^3.0 || ^4.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tighten\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tighten\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Use your Laravel named routes in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v2.6.3"
},
"time": "2026-06-23T21:57:52+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "v2.4.0",
@@ -11738,12 +11808,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2"
},
"platform-dev": [],
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

201
package-lock.json generated
View File

@@ -17,10 +17,12 @@
"pinia": "^3.0.4",
"pusher-js": "^8.5.0",
"ufo": "^1.6.1",
"v-onboarding": "^2.12.4",
"vue": "^3.5.25",
"vue-draggable-next": "^2.3.0",
"vue-icons-plus": "^0.1.9",
"vue3-apexcharts": "^1.11.1"
"vue3-apexcharts": "^1.11.1",
"ziggy-js": "^2.6.3"
},
"devDependencies": {
"@tailwindcss/language-server": "^0.14.29",
@@ -685,6 +687,16 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
@@ -1827,6 +1839,140 @@
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/integrations": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.1.tgz",
"integrity": "sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==",
"license": "MIT",
"dependencies": {
"@vueuse/core": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"async-validator": "^4",
"axios": "^1",
"change-case": "^4",
"drauu": "^0.3",
"focus-trap": "^7",
"fuse.js": "^6",
"idb-keyval": "^6",
"jwt-decode": "^3",
"nprogress": "^0.2",
"qrcode": "^1.5",
"sortablejs": "^1",
"universal-cookie": "^6"
},
"peerDependenciesMeta": {
"async-validator": {
"optional": true
},
"axios": {
"optional": true
},
"change-case": {
"optional": true
},
"drauu": {
"optional": true
},
"focus-trap": {
"optional": true
},
"fuse.js": {
"optional": true
},
"idb-keyval": {
"optional": true
},
"jwt-decode": {
"optional": true
},
"nprogress": {
"optional": true
},
"qrcode": {
"optional": true
},
"sortablejs": {
"optional": true
},
"universal-cookie": {
"optional": true
}
}
},
"node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"license": "MIT"
},
"node_modules/@vueuse/integrations/node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/integrations/node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.8"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/integrations/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz",
@@ -1904,7 +2050,7 @@
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/asynckit": {
@@ -2523,6 +2669,15 @@
}
}
},
"node_modules/focus-trap": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
"license": "MIT",
"dependencies": {
"tabbable": "^6.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -3366,6 +3521,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/qs-esm": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
"integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/request-light": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz",
@@ -3665,6 +3832,12 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/tabbable": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz",
"integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==",
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
@@ -3770,6 +3943,21 @@
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
"license": "MIT"
},
"node_modules/v-onboarding": {
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/v-onboarding/-/v-onboarding-2.12.4.tgz",
"integrity": "sha512-alfR0YSbE5v37JhrsDgxOxTIuHjBrVnwIWmwqvTtp0p66FSI/E5IEXgEOioelCd6RipwIUyZU4yeHocGzAIz2g==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.5",
"@vueuse/integrations": "^10.7.2",
"focus-trap": "^7",
"vue": "^3.2.21"
},
"peerDependencies": {
"vue": "^3.2.21"
}
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
@@ -4321,6 +4509,15 @@
"engines": {
"node": ">=12"
}
},
"node_modules/ziggy-js": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.3.tgz",
"integrity": "sha512-YcuXM0BohyWUsQcibMmlGMrfF01BQogRjrfg/AWQhG3rB/Swt76/SJxH5AH8X8Xn5aDWxGpYKdXV7Fo5g+BVLQ==",
"license": "MIT",
"dependencies": {
"qs-esm": "^7.0.3"
}
}
}
}

View File

@@ -30,9 +30,11 @@
"pinia": "^3.0.4",
"pusher-js": "^8.5.0",
"ufo": "^1.6.1",
"v-onboarding": "^2.12.4",
"vue": "^3.5.25",
"vue-draggable-next": "^2.3.0",
"vue-icons-plus": "^0.1.9",
"vue3-apexcharts": "^1.11.1"
"vue3-apexcharts": "^1.11.1",
"ziggy-js": "^2.6.3"
}
}

View File

@@ -0,0 +1,118 @@
<script setup>
import {ref, onMounted, nextTick, inject, computed, watch} from 'vue'
import { VOnboardingWrapper, VOnboardingStep } from 'v-onboarding'
import { useOnboardingStore } from '../../Stores/onboarding'
import { router } from '@inertiajs/vue3'
import { route } from 'ziggy-js'
import {NCard, NSpace, NButton, NTag} from 'naive-ui'
const wrapper = ref(null)
const store = useOnboardingStore()
const currentRoute = ref(route().current())
const currentSteps = computed(() => store.currentTourSteps)
onMounted(async () => {
if (store.isActive && store.activeStepIndex !== null) {
await nextTick()
wrapper.value?.goToStep(store.activeStepIndex)
}
})
// Следим за изменением индекса шага
watch(() => store.activeStepIndex, async (newIndex, oldIndex) => {
if (newIndex === null || !store.isActive) return
const steps = store.currentTourSteps
const step = steps[newIndex]
if (!step) return
const stepRoute = step.route
// Если шаг на другой странице — переходим
if (stepRoute && stepRoute !== currentRoute.value) {
// Переход на нужную страницу (индекс уже сохранён в сторе)
router.visit(route(stepRoute))
// Не возвращаем false, просто делаем переход
// После перехода onMounted восстановит шаг
} else {
wrapper.value?.goToStep(store.activeStepIndex)
}
}, { immediate: true }) // immediate, чтобы сработало при первом запуске
// Обработчик кнопки "Далее" в обычных шагах
const handleNext = (next, exit, isLast) => {
if (isLast) {
exit()
store.finish()
} else {
// Вызываем next() это переключит шаг внутри библиотеки,
// но нам также нужно обновить store.activeStepIndex, чтобы watch сработал
const nextIndex = store.activeStepIndex + 1
store.setStep(nextIndex) // обновляем стор
next() // говорим библиотеке переключиться
}
}
// Обработчик кнопки "Назад" в обычных шагах
const handlePrevious = (previous, exit, isFirst) => {
if (isFirst) {
} else {
// Вызываем next() это переключит шаг внутри библиотеки,
// но нам также нужно обновить store.activeStepIndex, чтобы watch сработал
const nextIndex = store.activeStepIndex - 1
store.setStep(nextIndex) // обновляем стор
previous() // говорим библиотеке переключиться
}
}
const onFinish = () => {
store.finish()
}
</script>
<template>
<VOnboardingWrapper
ref="wrapper"
:steps="currentSteps"
@finish="onFinish"
>
<template #default="{ step, next, previous, exit, isFirst, isLast }">
<VOnboardingStep>
<NCard
style="max-width: 320px;"
:title="step.content.title"
class="rounded-lg! mt-1"
size="small"
>
<p>{{ step.content.description }}</p>
<template #footer>
<NSpace justify="end">
<NButton
v-if="!isFirst"
size="small"
@click="handlePrevious(previous, exit, isFirst)"
>
Назад
</NButton>
<NButton
type="primary"
size="small"
:disabled="step.manual"
@click="handleNext(next, exit, isLast)"
>
{{ isLast ? 'Завершить' : 'Далее' }}
</NButton>
</NSpace>
</template>
</NCard>
</VOnboardingStep>
</template>
</VOnboardingWrapper>
</template>
<style>
:root {
--v-onboarding-overlay-z: 9999;
--v-onboarding-step-z: 9999;
}
</style>

View File

@@ -6,6 +6,7 @@ import AppDialogManager from "../Components/AppDialogManager.vue";
import Noise from "../Components/Noise.vue";
import { useThemeVars } from "naive-ui";
import { computed } from "vue";
import Onboarding from "../Components/Onboarding/Onboarding.vue";
const {isGlobalLoading} = useGlobalLoading()
const themeVars = useThemeVars()
@@ -91,6 +92,7 @@ const blobBg = computed(() => {
</NLayout>
</NLayout>
<Onboarding />
</template>
<style scoped>

View File

@@ -22,6 +22,7 @@ const hasHeaderExtra = computed(() => {
<!-- Лого -->
<NFlex align="center" :size="10" :wrap="false">
<component
id="app-logo"
:is="Link"
href="/"
style="text-decoration: none; display: flex; align-items: center; gap: 8px;"

View File

@@ -12,6 +12,15 @@ import SelectUserModal from "./Report/Components/SelectUserModal.vue"
import { Link, router } from "@inertiajs/vue3"
import { useServerTime } from "../Composables/useServerTime.js"
import { useThemeVars } from "naive-ui"
import {useOnboardingStore} from "../Stores/onboarding.js";
import {useModalStore} from "../Stores/modals.js";
const onboarding = useOnboardingStore()
const userStore = useAuthStore()
const modalStore = useModalStore()
// if (userStore.user.role.slug === 'dej')
// onboarding.startTour('welcomeDuty', 0)
const authStore = useAuthStore()
const themeVars = useThemeVars()
@@ -62,11 +71,7 @@ const showSelectUserModal = ref(false)
const showSelectNurseUserModal = ref(false)
const openReport = (path) => {
if (authStore.isDoctor) {
showSelectUserModal.value = true
} else {
router.visit(path)
}
modalStore.showSelectDutyUserModal = true
}
const openNurseReport = () => {
@@ -75,7 +80,7 @@ const openNurseReport = () => {
// } else {
// router.visit('/nurse/report')
// }
showSelectNurseUserModal.value = true
modalStore.showSelectNurseUserModal = true
}
const cardColor = computed(() => themeVars.value.cardColor)
@@ -88,7 +93,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
<NFlex vertical :size="10" class="max-w-2xl w-full" style="padding: 0 16px 24px;">
<!-- Профиль -->
<NEl class="profile-card rounded-2xl" style="padding: 18px 22px;">
<NEl id="user-panel" class="profile-card rounded-2xl" style="padding: 18px 22px;">
<NFlex justify="space-between" align="center" :wrap="false">
<NFlex align="center" :size="12" :wrap="false" style="min-width: 0; flex: 1;">
@@ -133,13 +138,15 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
<!-- Действия -->
<div class="bento-grid">
<ActionTile
id="new-report-duty"
v-if="authStore.hasPermission('report.create')"
:icon="TbArticle"
title="Заполнить сводную"
description="Ежедневный отчёт врача"
@click="openReport('/report')"
@click.prevent="openReport('/report')"
/>
<ActionTile
id="new-report-nurse"
v-if="authStore.hasPermission('nurse.report.view')"
:icon="TbStethoscope"
title="Журнал пациентов"
@@ -147,6 +154,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
@click="openNurseReport"
/>
<ActionTile
id="statistics"
v-if="authStore.hasPermission('stats.view')"
:icon="TbChartTreemap"
title="Статистика"
@@ -155,6 +163,7 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
href="/statistic"
/>
<ActionTile
id="reports"
v-if="authStore.hasPermission('report.view') || authStore.hasPermission('nurse.report.view')"
:icon="TbReportMedical"
title="Отчёты"
@@ -183,9 +192,9 @@ const dividerColor = computed(() => themeVars.value.dividerColor)
</NFlex>
</div>
<SelectUserModal v-model:show="showSelectUserModal" :only-user-department="false" />
<SelectUserModal v-model:show="modalStore.showSelectDutyUserModal" :only-user-department="false" />
<SelectUserModal
v-model:show="showSelectNurseUserModal"
v-model:show="modalStore.showSelectNurseUserModal"
target-path="/nurse/report"
submit-label="Перейти к журналу пациентов"
:with-exists-check="false"

View File

@@ -4,6 +4,7 @@ import {useReportStore} from "../../../Stores/report.js";
import {computed, ref, watch} from "vue";
import {router, Link} from "@inertiajs/vue3";
import {useDebounceFn} from "@vueuse/core";
import {useOnboardingStore} from "../../../Stores/onboarding.js";
const show = defineModel('show')
const props = defineProps({
@@ -176,6 +177,19 @@ const onChangeDepartment = (departmentId) => {
usersLoaded.value = false
fetchUsers(departmentId)
}
const onboarding = useOnboardingStore()
const onAfterEnter = () => {
// Если онбординг активен и текущий шаг имеет opensModal, переходим к следующему
if (onboarding.isActive) {
const currentSteps = onboarding.currentTourSteps
const currentStep = currentSteps[onboarding.activeStepIndex]
if (currentStep?.opensModal) {
console.log(currentStep)
onboarding.setStep(onboarding.activeStepIndex + 1) // переходим на шаг внутри модалки
}
}
}
</script>
<template>
@@ -184,9 +198,10 @@ const onChangeDepartment = (departmentId) => {
class="max-w-[420px]"
:mask-closable="false"
@before-leave="onAfterLeave"
@after-enter="onAfterEnter"
>
<NForm id="select-user-form" ref="formRef" :model="reportStore.reportInfo" :rules="rules">
<NFormItem label="Выберите отделение" path="departmentId">
<NFormItem id="modal-select-user-department" label="Выберите отделение" path="departmentId">
<NSelect :options="departments"
:loading="loadingDepartmentFetch"
@change="onChangeDepartment"
@@ -194,7 +209,7 @@ const onChangeDepartment = (departmentId) => {
filterable
/>
</NFormItem>
<NFormItem label="Выберите ответственного" path="userId">
<NFormItem id="modal-select-user-doctor" label="Выберите ответственного" path="userId">
<NSelect :options="users"
:loading="loadingUserFetch"
:disabled="!usersLoaded"
@@ -211,6 +226,7 @@ const onChangeDepartment = (departmentId) => {
</NAlert>
<template #action>
<NButton form-id="select-user-form"
id="modal-select-user-submit"
type="primary"
block
@click="onSubmit"

View File

@@ -0,0 +1,13 @@
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import {tours} from "../config/tours.js";
export const useModalStore = defineStore('modals', () => {
const showSelectDutyUserModal = ref(false)
const showSelectNurseUserModal = ref(false)
return {
showSelectDutyUserModal,
showSelectNurseUserModal,
}
})

View File

@@ -0,0 +1,40 @@
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import {tours} from "../config/tours.js";
export const useOnboardingStore = defineStore('onboarding', () => {
const activeTourId = ref(null)
const activeStepIndex = ref(0)
const isActive = ref(false)
const currentTourSteps = computed(() => {
if (!activeTourId.value) return []
return tours[activeTourId.value]?.steps || []
})
const startTour = (tourId, startFrom = 0) => {
activeTourId.value = tourId
activeStepIndex.value = startFrom
isActive.value = true
}
const setStep = (index) => {
activeStepIndex.value = index
}
const finish = () => {
activeTourId.value = null
activeStepIndex.value = 0
isActive.value = false
}
return {
activeTourId,
activeStepIndex,
isActive,
currentTourSteps,
startTour,
finish,
setStep
}
})

View File

@@ -7,6 +7,7 @@ import {startGlobalLoading, stopGlobalLoading} from "./Composables/useGlobalLoad
import './bootstrap';
import '../css/app.css';
import {NConfigProvider, NDialogProvider, NMessageProvider, ruRU, dateRuRU, darkTheme} from "naive-ui";
import { ZiggyVue } from 'ziggy-js';
router.on('start', () => {
startGlobalLoading()
@@ -64,6 +65,7 @@ createInertiaApp({
const pinia = createPinia()
vueApp.use(ZiggyVue)
vueApp.use(plugin)
vueApp.use(pinia)

View File

@@ -0,0 +1,113 @@
import {useModalStore} from "../Stores/modals.js";
import {useOnboardingStore} from "../Stores/onboarding.js";
export const tours = {
// Приветственный тур для новых пользователей
welcomeDuty: {
id: 'welcome-duty',
name: 'Знакомство с Метрикой',
steps: [
{
attachTo: { element: '#user-panel' },
route: 'start', // главная страница
content: {
title: '👋 Добро пожаловать!',
description: 'Это ваша панель управления. Сейчас мы покажем вам основные возможности.'
},
popper: { placement: 'bottom-start' }, // опционально: позиция тултипа
on: {
afterStep: () => {
const modalStore = useModalStore()
modalStore.showSelectDutyUserModal = false
},
beforeStep: () => {
const modalStore = useModalStore()
modalStore.showSelectDutyUserModal = false
},
}
},
{
attachTo: { element: '#new-report-duty' },
route: 'start', // переходим на страницу отчётов
content: {
title: '✏️ Создать отчёт',
description: 'Нажмите эту кнопку, чтобы начать создание нового отчёта.'
},
on: {
afterStep: () => {
const modalStore = useModalStore()
const onboarding = useOnboardingStore()
if (onboarding.activeStepIndex === 2) modalStore.showSelectDutyUserModal = true
if (onboarding.activeStepIndex < 2) modalStore.showSelectDutyUserModal = false
},
beforeStep: () => {
const modalStore = useModalStore()
const onboarding = useOnboardingStore()
if (onboarding.activeStepIndex < 2) modalStore.showSelectDutyUserModal = false
},
},
},
{
attachTo: { element: '#modal-select-user-department' },
route: 'start',
content: { title: 'Выберите отделение', description: 'Можете использовать список, либо ввести ФИО для поиска' },
insideModal: true, // флаг, что этот шаг внутри модалки
},
{
attachTo: { element: '#modal-select-user-doctor' },
route: 'start',
content: { title: 'Введите ответственного', description: 'Можете использовать список, либо ввести ФИО для поиска' },
insideModal: true, // флаг, что этот шаг внутри модалки
},
{
attachTo: { element: '#modal-select-user-submit' },
route: 'start',
content: { title: 'Подтвердите ввод' },
insideModal: true, // флаг, что этот шаг внутри модалки
manual: true
},
{
attachTo: { element: '#report-name-input' },
route: 'report', // страница создания отчёта
content: {
title: '📝 Название отчёта',
description: 'Введите название, чтобы легко находить отчёт позже.'
}
},
{
attachTo: { element: '#save-report-btn' },
route: 'reports.create',
content: {
title: '💾 Сохранить',
description: 'После заполнения всех полей нажмите "Сохранить", чтобы завершить.'
}
}
]
},
// Тур по новой функции "Экспорт"
duty: {
id: 'featureX',
name: 'Новая функция: Экспорт',
steps: [
{
attachTo: { element: '#export-excel-btn' },
route: 'reports.index',
content: {
title: '📤 Экспорт в Excel',
description: 'Теперь вы можете выгрузить отчёт в Excel одним кликом.'
}
},
{
attachTo: { element: '#export-pdf-btn' },
route: 'reports.index',
content: {
title: '📄 Экспорт в PDF',
description: 'А также сохранить отчёт в формате PDF для печати.'
}
}
]
}
}

View File

@@ -16,6 +16,7 @@
</script>
<!-- Styles / Scripts -->
@routes
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/js/app.js'])
@inertiaHead

View File

@@ -17,4 +17,9 @@ export default defineConfig({
ignored: ['**/storage/framework/views/**'],
},
},
resolve: {
alias: {
'@': '/resources/js',
},
},
});