init
This commit is contained in:
34
src/app/App.vue
Normal file
34
src/app/App.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<transition-fade-component>
|
||||
<sidebar-component v-if="!isHideSidebar" />
|
||||
</transition-fade-component>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-page" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<modals-component />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import {
|
||||
Modals as ModalsComponent,
|
||||
Sidebar as SidebarComponent,
|
||||
} from '@/widgets'
|
||||
import { useUserStore } from '@/entities'
|
||||
import { Routes } from '@/shared'
|
||||
|
||||
const routesWithoutSidebar: Routes[] = [Routes.LOGIN]
|
||||
|
||||
const { setCurrentUser } = useUserStore()
|
||||
const route = useRoute()
|
||||
|
||||
const isHideSidebar = computed(() =>
|
||||
routesWithoutSidebar.includes(route.name as Routes),
|
||||
)
|
||||
onMounted(async () => {
|
||||
await setCurrentUser()
|
||||
})
|
||||
</script>
|
7
src/app/configs.ts
Normal file
7
src/app/configs.ts
Normal file
@ -0,0 +1,7 @@
|
||||
const mainHost = import.meta.env.MAIN_HOST || 'https://human.dmtay.ru'
|
||||
|
||||
export default {
|
||||
mainHost,
|
||||
baseURL: mainHost + '/api/',
|
||||
medicalURL: mainHost + '/medical/',
|
||||
}
|
143
src/app/createApp.ts
Normal file
143
src/app/createApp.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import VueDatePicker from '@vuepic/vue-datepicker'
|
||||
import { createApp, defineAsyncComponent, h } from 'vue'
|
||||
import Vue3Toasity, { type ToastContainerOptions } from 'vue3-toastify'
|
||||
|
||||
import App from '@/app/App.vue'
|
||||
|
||||
import { PageLayout } from '@/widgets'
|
||||
import {
|
||||
AuthCard,
|
||||
AuthForm,
|
||||
Button,
|
||||
ButtonMenu,
|
||||
Card,
|
||||
FileCard,
|
||||
IconBase,
|
||||
type IconNames,
|
||||
InfinityLoading,
|
||||
Input,
|
||||
Link,
|
||||
Logo,
|
||||
Spinner,
|
||||
Tabs,
|
||||
Tag,
|
||||
ToastIcon,
|
||||
Tooltip,
|
||||
TransitionFade,
|
||||
UserBase,
|
||||
Dropdown,
|
||||
Textarea,
|
||||
} from '@/shared'
|
||||
|
||||
export const create = () => {
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(Vue3Toasity, {
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
pauseOnHover: false,
|
||||
icon: ({ type }) =>
|
||||
h(ToastIcon, {
|
||||
view: type,
|
||||
}),
|
||||
closeButton: ({ closeToast }) =>
|
||||
h(IconBase, {
|
||||
name: 'close' as IconNames,
|
||||
class: 'Toastify__close-button',
|
||||
onClick: closeToast,
|
||||
}),
|
||||
} as ToastContainerOptions)
|
||||
|
||||
app.component('VueDatePicker', VueDatePicker)
|
||||
app.component('icon-base-component', IconBase)
|
||||
app.component('button-component', Button)
|
||||
app.component('card-component', Card)
|
||||
app.component('file-card-component', FileCard)
|
||||
app.component('transition-fade-component', TransitionFade)
|
||||
app.component('input-component', Input)
|
||||
app.component('page-layout-component', PageLayout)
|
||||
app.component('user-base-component', UserBase)
|
||||
app.component('link-component', Link)
|
||||
app.component('button-menu-component', ButtonMenu)
|
||||
app.component('tag-component', Tag)
|
||||
app.component('tooltip-component', Tooltip)
|
||||
app.component('spinner-component', Spinner)
|
||||
app.component('tabs-component', Tabs)
|
||||
app.component('infinity-loading-component', InfinityLoading)
|
||||
app.component('logo-component', Logo)
|
||||
app.component('auth-form-component', AuthForm)
|
||||
app.component('auth-card-component', AuthCard)
|
||||
app.component('dropdown-component', Dropdown)
|
||||
app.component('textarea-component', Textarea)
|
||||
|
||||
//modals
|
||||
app.component(
|
||||
'modal-dialog-component',
|
||||
defineAsyncComponent(
|
||||
() => import('@/widgets/modals/ui/ModalDialog/ModalDialog.vue'),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-edit-patient-component',
|
||||
defineAsyncComponent(
|
||||
() => import('@/widgets/modals/ui/EditPatient/EditPatient.vue'),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-select-questionnaire-component',
|
||||
defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/widgets/modals/ui/SelectQuestionnaires/SelectQuestionnaires.vue'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-add-destination-component',
|
||||
defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/widgets/modals/ui/ModalAddDestination/ModalAddDestination.vue'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-add-reminder-component',
|
||||
defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/widgets/modals/ui/ModalAddReminder/ModalAddReminder.vue'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-view-questionnaire-component',
|
||||
defineAsyncComponent(
|
||||
() =>
|
||||
import(
|
||||
'@/widgets/modals/ui/ModalViewQuestionnaire/ModalViewQuestionnaire.vue'
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-analysis-hint-component',
|
||||
defineAsyncComponent(
|
||||
() => import('@/widgets/modals/ui/AnalysisHints/AnalysisHints.vue'),
|
||||
),
|
||||
)
|
||||
|
||||
app.component(
|
||||
'modal-view-optimums-component',
|
||||
defineAsyncComponent(
|
||||
() => import('@/widgets/modals/ui/ViewOptimums/ViewOptimums.vue'),
|
||||
),
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
16
src/app/directives/clickOutside.ts
Normal file
16
src/app/directives/clickOutside.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Directive } from 'vue'
|
||||
|
||||
export const clickOutside: Directive = {
|
||||
mounted(element, { value }) {
|
||||
element.clickOutside = function (event: Event) {
|
||||
if (!(element === event.target || element.contains(event.target))) {
|
||||
value(event)
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', element.clickOutside)
|
||||
},
|
||||
unmounted(element) {
|
||||
document.body.removeEventListener('click', element.clickOutside)
|
||||
},
|
||||
}
|
1
src/app/directives/index.ts
Normal file
1
src/app/directives/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './clickOutside'
|
16
src/app/main.ts
Normal file
16
src/app/main.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import '@/app/styles/index.scss'
|
||||
import { createPinia } from 'pinia'
|
||||
import VueLazyLoad from 'vue3-lazyload'
|
||||
import { create } from '@/app/createApp'
|
||||
import { router } from '@/app/providers/router'
|
||||
import { clickOutside } from './directives'
|
||||
|
||||
export const app = create()
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.directive('click-outside', clickOutside)
|
||||
app.use(VueLazyLoad, {
|
||||
delay: 300,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
44
src/app/providers/router/index.ts
Normal file
44
src/app/providers/router/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { type Middleware, type MiddlewareContext } from './middlewares'
|
||||
import { routes } from './routes'
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (!to.meta.middleware) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const middleware = to.meta.middleware as Middleware[]
|
||||
|
||||
const context: MiddlewareContext = {
|
||||
to,
|
||||
from,
|
||||
next,
|
||||
}
|
||||
|
||||
return middleware[0]({
|
||||
...context,
|
||||
next: middlewarePipeline(context, middleware, 1),
|
||||
})
|
||||
})
|
||||
|
||||
const middlewarePipeline = (
|
||||
context: MiddlewareContext,
|
||||
middleware: Middleware[],
|
||||
index: number,
|
||||
) => {
|
||||
const nextMiddleware = middleware[index]
|
||||
|
||||
if (!nextMiddleware) {
|
||||
return context.next
|
||||
}
|
||||
|
||||
return () => {
|
||||
const nextPipeline = middlewarePipeline(context, middleware, index + 1)
|
||||
nextMiddleware({ ...context, next: nextPipeline })
|
||||
}
|
||||
}
|
43
src/app/providers/router/middlewares.ts
Normal file
43
src/app/providers/router/middlewares.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
|
||||
import { tokenValidate } from '@/features'
|
||||
import { Routes } from '@/shared'
|
||||
|
||||
export type MiddlewareContext = {
|
||||
to: RouteLocationNormalized
|
||||
from: RouteLocationNormalized
|
||||
next: NavigationGuardNext
|
||||
}
|
||||
|
||||
export type Middleware = (context: MiddlewareContext) => any
|
||||
|
||||
export const guest = async ({ next }: MiddlewareContext) => {
|
||||
const isAuth = await tokenValidate()
|
||||
|
||||
if (isAuth) {
|
||||
return next({
|
||||
name: Routes.INDEX,
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const auth = async ({ next }: MiddlewareContext) => {
|
||||
const isAuth = await tokenValidate()
|
||||
|
||||
if (!isAuth) {
|
||||
return next({
|
||||
name: Routes.LOGIN,
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const patient = ({ to, next }: MiddlewareContext) => {
|
||||
if (to.params.id) {
|
||||
return next()
|
||||
}
|
||||
|
||||
return next({ name: Routes.MY_PATIENTS })
|
||||
}
|
124
src/app/providers/router/routes.ts
Normal file
124
src/app/providers/router/routes.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { Routes } from '@/shared'
|
||||
import { auth, guest, patient } from './middlewares'
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: Routes.INDEX,
|
||||
redirect: { name: Routes.MY_PATIENTS },
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/patient/:id?',
|
||||
name: Routes.PATIENT,
|
||||
props: true,
|
||||
component: () => import('@/pages/patient/ui/Patient/Patient.vue'),
|
||||
meta: {
|
||||
middleware: [auth, patient],
|
||||
page: {
|
||||
title: 'Пациента',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/my-patients',
|
||||
name: Routes.MY_PATIENTS,
|
||||
component: () =>
|
||||
import('@/pages/patients/ui/MyPatients/MyPatients.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Мои пациенты',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/requests-patients',
|
||||
name: Routes.REQUESTS_PATIENTS,
|
||||
component: () =>
|
||||
import('@/pages/patients/ui/RequestsPatients/RequestsPatients.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Запросы на ведение',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/calendar',
|
||||
name: Routes.CALENDAR,
|
||||
component: () => import('@/pages/calendar/ui/Calendar/Calendar.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Календарь',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
name: Routes.CHAT,
|
||||
component: () => import('@/pages/chat/ui/Chat/Chat.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Чат',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/videochat',
|
||||
name: Routes.VIDEOCHAT,
|
||||
component: () => import('@/pages/videochat/ui/Videochat/Videochat.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Видеочат',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: Routes.LIBRARY,
|
||||
component: () => import('@/pages/library/ui/Library/Library.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Моя библиотека',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: Routes.LOGIN,
|
||||
component: () => import('@/pages/login/ui/LoginPage/LoginPage.vue'),
|
||||
meta: {
|
||||
middleware: [guest],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/support',
|
||||
name: Routes.SUPPORT,
|
||||
component: () => import('@/pages/support/ui/Support/Support.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Поддержка',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: Routes.PROFILE,
|
||||
component: () => import('@/pages/profile/ui/Profile/Profile.vue'),
|
||||
meta: {
|
||||
middleware: [auth],
|
||||
page: {
|
||||
title: 'Профиль',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
16
src/app/styles/index.scss
Normal file
16
src/app/styles/index.scss
Normal file
@ -0,0 +1,16 @@
|
||||
@import './utils/utils';
|
||||
@import './utils/reset';
|
||||
@import './utils/common';
|
||||
|
||||
@import './utils/fonts';
|
||||
@import './utils/icons';
|
||||
@import './utils/toast';
|
||||
@import './utils/datetimepocker';
|
||||
|
||||
@import '../../shared/shared';
|
||||
@import '../../entities/entities';
|
||||
@import '../../features/features';
|
||||
@import '../../widgets/widgets';
|
||||
@import '../../pages/pages';
|
||||
|
||||
@import './utils/animations';
|
46
src/app/styles/utils/animations.scss
Normal file
46
src/app/styles/utils/animations.scss
Normal file
@ -0,0 +1,46 @@
|
||||
// fade
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// fade-page
|
||||
.fade-page-enter-active,
|
||||
.fade-page-leave-active {
|
||||
transition: opacity 0.5s linear !important;
|
||||
}
|
||||
|
||||
.fade-page-enter-from,
|
||||
.fade-page-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// list
|
||||
.list-item {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
@keyframes rotate360 {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
121
src/app/styles/utils/common.scss
Normal file
121
src/app/styles/utils/common.scss
Normal file
@ -0,0 +1,121 @@
|
||||
body {
|
||||
font-size: toRem(14);
|
||||
color: var(--dark-main);
|
||||
font-family: $mainFontFamily, sans-serif;
|
||||
font-weight: 400;
|
||||
background-color: var(--white);
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&[lazy='loading'] {
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&[lazy='error'] {
|
||||
}
|
||||
|
||||
&[lazy='loaded'] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--bright-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
&::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--dark-64);
|
||||
background-clip: content-box;
|
||||
border: 5px solid transparent;
|
||||
border-radius: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(100%);
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.row {
|
||||
@include row(toRem(4));
|
||||
}
|
||||
|
||||
.column {
|
||||
@include column(toRem(4));
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
76
src/app/styles/utils/datetimepocker.scss
Normal file
76
src/app/styles/utils/datetimepocker.scss
Normal file
@ -0,0 +1,76 @@
|
||||
@import '@vuepic/vue-datepicker/dist/main.css';
|
||||
|
||||
.dp {
|
||||
$b: &;
|
||||
|
||||
font-family: $mainFontFamily !important;
|
||||
&__theme_light {
|
||||
--dp-font-size: toRem(15);
|
||||
--dp-text-color: var(--dark-main);
|
||||
// --dp-background-color: var(--grey-main);
|
||||
--dp-input-padding: 12px 30px 12px 12px;
|
||||
--dp-icon-color: var(--brand-main);
|
||||
--dp-hover-icon-color: var(--brand-main);
|
||||
--dp-hover-text-color: var(--dark-main);
|
||||
--dp-primary-color: var(--brand-main);
|
||||
}
|
||||
|
||||
&__main {
|
||||
height: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
|
||||
&_wrap {
|
||||
height: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__month_year_wrap {
|
||||
font-size: toRem(17);
|
||||
#{$b}__btn:last-child {
|
||||
color: var(--brand-main);
|
||||
}
|
||||
}
|
||||
|
||||
&__today {
|
||||
border: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 2px;
|
||||
bottom: 5px;
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
&__calendar {
|
||||
font-size: toRem(15);
|
||||
&_header {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&__container_block {
|
||||
font-size: toRem(15);
|
||||
}
|
||||
|
||||
&__overlay_container {
|
||||
font-size: toRem(15);
|
||||
}
|
||||
}
|
6
src/app/styles/utils/fonts.scss
Normal file
6
src/app/styles/utils/fonts.scss
Normal file
@ -0,0 +1,6 @@
|
||||
@include newFont($mainFontFamily, 'NeueHaasUnica-Regular', 400);
|
||||
@include newFont($mainFontFamily, 'NeueHaasUnica-Medium', 500);
|
||||
@include newFont($mainFontFamily, 'NeueHaasUnica-Bold', 700);
|
||||
@include newFont($mainFontFamily, 'NeueHaasUnica-Black', 900);
|
||||
|
||||
@include newFont($decorationFontFamily, 'TinkoffSans-Bold', 700);
|
40
src/app/styles/utils/functions.scss
Normal file
40
src/app/styles/utils/functions.scss
Normal file
@ -0,0 +1,40 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@function toRem($size) {
|
||||
$remSize: math.div($size, 12) * 1rem;
|
||||
|
||||
@return $remSize;
|
||||
}
|
||||
|
||||
@mixin fontSize($name, $options: ()) {
|
||||
@each $tagName, $tagValue in $tags {
|
||||
@if $tagName == $name {
|
||||
$selectedTag: map-get($tags, $tagName);
|
||||
$fontSize: nth($selectedTag, 1);
|
||||
$lineHeight: nth($selectedTag, 2);
|
||||
|
||||
font-size: $fontSize;
|
||||
line-height: $lineHeight;
|
||||
|
||||
@if map-get($options, weight) {
|
||||
font-weight: map-get($options, weight);
|
||||
}
|
||||
|
||||
@if map-get($options, style) {
|
||||
font-style: map-get($options, style);
|
||||
}
|
||||
|
||||
@if map-get($options, uppercase) {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@if map-get($options, letter-spacing) {
|
||||
letter-spacing: map-get($options, letter-spacing);
|
||||
}
|
||||
|
||||
@if map-get($options, line-height) {
|
||||
line-height: map-get($options, line-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
128
src/app/styles/utils/icons.scss
Normal file
128
src/app/styles/utils/icons.scss
Normal file
@ -0,0 +1,128 @@
|
||||
@font-face {
|
||||
font-family: "icons";
|
||||
src: url("/fonts/icons/icons.woff?98e4dbf90127ac6ec671d6317de9533e") format("woff"),
|
||||
url("/fonts/icons/icons.woff2?98e4dbf90127ac6ec671d6317de9533e") format("woff2");
|
||||
}
|
||||
|
||||
i[class^="icon-"]:before, i[class*=" icon-"]:before {
|
||||
font-family: icons !important;
|
||||
font-style: normal;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-arrow-down:before {
|
||||
content: "\f101";
|
||||
}
|
||||
.icon-arrow-left:before {
|
||||
content: "\f102";
|
||||
}
|
||||
.icon-arrow-narrow-down:before {
|
||||
content: "\f103";
|
||||
}
|
||||
.icon-arrow-narrow-up-right:before {
|
||||
content: "\f104";
|
||||
}
|
||||
.icon-arrow-narrow-up:before {
|
||||
content: "\f105";
|
||||
}
|
||||
.icon-arrow-right:before {
|
||||
content: "\f106";
|
||||
}
|
||||
.icon-bell:before {
|
||||
content: "\f107";
|
||||
}
|
||||
.icon-book-open:before {
|
||||
content: "\f108";
|
||||
}
|
||||
.icon-calculator:before {
|
||||
content: "\f109";
|
||||
}
|
||||
.icon-calendar:before {
|
||||
content: "\f10a";
|
||||
}
|
||||
.icon-camera:before {
|
||||
content: "\f10b";
|
||||
}
|
||||
.icon-check-circle:before {
|
||||
content: "\f10c";
|
||||
}
|
||||
.icon-check-heart:before {
|
||||
content: "\f10d";
|
||||
}
|
||||
.icon-check:before {
|
||||
content: "\f10e";
|
||||
}
|
||||
.icon-clock:before {
|
||||
content: "\f10f";
|
||||
}
|
||||
.icon-close:before {
|
||||
content: "\f110";
|
||||
}
|
||||
.icon-date:before {
|
||||
content: "\f111";
|
||||
}
|
||||
.icon-dots-vertical:before {
|
||||
content: "\f112";
|
||||
}
|
||||
.icon-file:before {
|
||||
content: "\f113";
|
||||
}
|
||||
.icon-help-circle:before {
|
||||
content: "\f114";
|
||||
}
|
||||
.icon-info-circle:before {
|
||||
content: "\f115";
|
||||
}
|
||||
.icon-info:before {
|
||||
content: "\f116";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\f117";
|
||||
}
|
||||
.icon-message-text:before {
|
||||
content: "\f118";
|
||||
}
|
||||
.icon-pencil-line:before {
|
||||
content: "\f119";
|
||||
}
|
||||
.icon-placeholder:before {
|
||||
content: "\f11a";
|
||||
}
|
||||
.icon-plus:before {
|
||||
content: "\f11b";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\f11c";
|
||||
}
|
||||
.icon-switch-vertical:before {
|
||||
content: "\f11d";
|
||||
}
|
||||
.icon-trash:before {
|
||||
content: "\f11e";
|
||||
}
|
||||
.icon-tui-marker:before {
|
||||
content: "\f11f";
|
||||
}
|
||||
.icon-user-edit:before {
|
||||
content: "\f120";
|
||||
}
|
||||
.icon-user-plus:before {
|
||||
content: "\f121";
|
||||
}
|
||||
.icon-user:before {
|
||||
content: "\f122";
|
||||
}
|
||||
.icon-users-right:before {
|
||||
content: "\f123";
|
||||
}
|
||||
.icon-video-recorder:before {
|
||||
content: "\f124";
|
||||
}
|
||||
.icon-x-circle:before {
|
||||
content: "\f125";
|
||||
}
|
111
src/app/styles/utils/mixins.scss
Normal file
111
src/app/styles/utils/mixins.scss
Normal file
@ -0,0 +1,111 @@
|
||||
@mixin newFont($family, $pathName, $weight) {
|
||||
@font-face {
|
||||
font-family: '#{$family}';
|
||||
src:
|
||||
url('/fonts/#{$pathName}.woff2') format('woff2'),
|
||||
url('/fonts/#{$pathName}.woff') format('woff');
|
||||
font-weight: #{$weight};
|
||||
font-display: swap;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@mixin center($position: 'both') {
|
||||
position: absolute;
|
||||
|
||||
@if $position == 'vertical' {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
} @else if $position == 'horizontal' {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
} @else if $position == 'both' {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
} @else if $position == 'stretch' {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin column($gap: 0px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
@mixin row($gap: 0px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
@mixin responsive($breakpoint) {
|
||||
@if $breakpoint == 'sm' {
|
||||
@media only screen and (max-width: 576px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == 'md' {
|
||||
@media only screen and (max-width: 768px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == 'lg' {
|
||||
@media only screen and (max-width: 992px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == 'xl' {
|
||||
@media only screen and (max-width: 1200px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@if $breakpoint == 'xxl' {
|
||||
@media only screen and (max-width: 1400px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin modalBaseStyles() {
|
||||
&__content {
|
||||
@include center();
|
||||
@include column();
|
||||
width: toRem(490);
|
||||
background-color: var(--white);
|
||||
border-radius: $borderRadius24;
|
||||
padding: toRem(32);
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include fontSize(
|
||||
h2,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-top: toRem(20);
|
||||
|
||||
@include fontSize(s-13);
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: toRem(12);
|
||||
margin-top: toRem(24);
|
||||
}
|
||||
}
|
571
src/app/styles/utils/reset.scss
Normal file
571
src/app/styles/utils/reset.scss
Normal file
@ -0,0 +1,571 @@
|
||||
//** * Modern CSS Reset Tweaks * ================================================== */
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
&:focus-within {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
text-size-adjust: 100%;
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeSpeed;
|
||||
}
|
||||
|
||||
/* Box sizing normalization */
|
||||
*,
|
||||
::after,
|
||||
::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Elements that don't have a class get default styles */
|
||||
a:not([class]) {
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS Reset Tweaks
|
||||
*
|
||||
* http://meyerweb.com/eric/tools/css/reset/
|
||||
* v2.0-modified | 20110126
|
||||
* License: none (public domain)
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* make sure to set some focus styles for accessibility */
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
main,
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Input Reset
|
||||
*/
|
||||
input:required,
|
||||
input {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-box-shadow: 0 0 0 30px white inset;
|
||||
}
|
||||
|
||||
input[type='search']::-webkit-search-cancel-button,
|
||||
input[type='search']::-webkit-search-decoration,
|
||||
input[type='search']::-webkit-search-results-button,
|
||||
input[type='search']::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
input[type='search'] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
vertical-align: top;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||
*/
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
|
||||
*/
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Make images easier to work with */
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make pictures easier to work with */
|
||||
picture {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
button,
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
|
||||
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
html input[type='button'],
|
||||
input[type='reset'],
|
||||
input[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address box sizing set to content-box in IE 8/9.
|
||||
*/
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
input[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||
* on OS X.
|
||||
*/
|
||||
input[type='search']::-webkit-search-cancel-button,
|
||||
input[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 3+.
|
||||
*/
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
vertical-align: top;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on normalize.css v8.0.1
|
||||
* github.com/necolas/normalize.css
|
||||
*/
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
overflow: visible;
|
||||
background: #000;
|
||||
border: 0;
|
||||
height: 1px;
|
||||
line-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
page-break-after: always;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
*/
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
small {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -5px;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
button,
|
||||
[type='button'],
|
||||
[type='reset'],
|
||||
[type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type='button']::-moz-focus-inner,
|
||||
[type='reset']::-moz-focus-inner,
|
||||
[type='submit']::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
|
||||
display: block;
|
||||
border: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body:not(:-moz-handler-blocked) fieldset {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
[type='number']::-webkit-inner-spin-button,
|
||||
[type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
[type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
* Misc
|
||||
* ========================================================================== */
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
43
src/app/styles/utils/toast.scss
Normal file
43
src/app/styles/utils/toast.scss
Normal file
@ -0,0 +1,43 @@
|
||||
@import 'vue3-toastify/dist/index.css';
|
||||
|
||||
:root {
|
||||
--toastify-text-color-light: var(--dark-main);
|
||||
--toastify-toast-min-height: toRem(48);
|
||||
}
|
||||
|
||||
.Toastify {
|
||||
font-family: $mainFontFamily, sans-serif;
|
||||
|
||||
&__toast {
|
||||
border-radius: $borderRadius8;
|
||||
box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.12);
|
||||
padding: toRem(12) toRem(16);
|
||||
|
||||
&-body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-content {
|
||||
@include fontSize(s-13);
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: toRem(16);
|
||||
}
|
||||
|
||||
&--success {
|
||||
background-color: var(--green-bg);
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: var(--critical-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
color: var(--dark-main);
|
||||
font-size: toRem(24);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
8
src/app/styles/utils/utils.scss
Normal file
8
src/app/styles/utils/utils.scss
Normal file
@ -0,0 +1,8 @@
|
||||
@import 'functions';
|
||||
|
||||
@import 'vars/colors';
|
||||
@import 'vars/other';
|
||||
@import 'vars/spaces';
|
||||
@import 'vars/typography';
|
||||
|
||||
@import 'mixins';
|
71
src/app/styles/utils/vars/colors.scss
Normal file
71
src/app/styles/utils/vars/colors.scss
Normal file
@ -0,0 +1,71 @@
|
||||
:root {
|
||||
--white: #fff;
|
||||
|
||||
// main
|
||||
--dark-main: #364153;
|
||||
--berry-main: #ef6093;
|
||||
--grey-main: #f2f5f9;
|
||||
--brand-main: #a241f0;
|
||||
--berry-main: #ef6093;
|
||||
--orange-main: #fab619;
|
||||
--purple-main: #775abf;
|
||||
--green-main: #b2d677;
|
||||
--critical-main: #ed6a5eff;
|
||||
|
||||
//border
|
||||
--grey-border: #dfe4ed;
|
||||
--green-border: rgba(135, 163, 88, 1);
|
||||
|
||||
//background
|
||||
--green-bg: rgba(236, 244, 225, 1);
|
||||
--critical-bg: #f6c2bd;
|
||||
--brand-4-bg: rgba(162, 65, 240, 0.04); //#F6EEFF
|
||||
--blue-20-bg: rgba(134, 200, 241, 0.2); //#f4f5fd
|
||||
--purple-bg: #f6eeff;
|
||||
--purple-8-bg: rgba(119, 90, 191, 0.08);
|
||||
|
||||
//hover
|
||||
--brand-hover: #9138d8;
|
||||
--grey-hover: #eaeffa;
|
||||
|
||||
//states
|
||||
--berry-8: rgba(239, 96, 147, 0.08);
|
||||
--dark-4: rgba(54, 65, 83, 0.04);
|
||||
--dark-14: rgba(54, 65, 83, 0.14);
|
||||
--dark-20: rgba(54, 65, 83, 0.2);
|
||||
--berry-14: rgba(239, 96, 147, 0.14);
|
||||
--berry-20: rgba(239, 96, 147, 0.2);
|
||||
--dark-32: rgba(54, 65, 83, 0.32);
|
||||
--green-32: rgba(206, 248, 137, 0.32);
|
||||
--green-64: rgba(178, 214, 119, 0.64);
|
||||
--dark-64: #364153a3;
|
||||
--grey-64: #f8fafc;
|
||||
--blue-20: #86c8f133;
|
||||
--brand-20: rgba(162, 65, 240, 0.2);
|
||||
--brand-64: rgba(162, 65, 240, 0.64);
|
||||
--brand-8: rgba(162, 65, 240, 0.08);
|
||||
--brand-4: rgba(162, 65, 240, 0.04);
|
||||
--dark-blue: #3388bc;
|
||||
--blue-14: rgba(134, 200, 241, 0.14);
|
||||
--green-14: rgba(178, 214, 119, 0.14);
|
||||
--green-20: rgba(178, 214, 119, 0.2);
|
||||
--orange-20: rgba(255, 219, 117, 0.2);
|
||||
--orange-32: rgba(255, 219, 117, 0.32);
|
||||
--berry-4: rgba(239, 96, 147, 0.04);
|
||||
--berry-8: rgba(239, 96, 147, 0.08);
|
||||
--berry-14: rgba(239, 96, 147, 0.14);
|
||||
|
||||
--text-primary: rgba(0, 0, 0, 0.8);
|
||||
--day-Base-base-04: #dddfe0;
|
||||
|
||||
// gradients
|
||||
--brand-linear: linear-gradient(138deg, #a241f0 2.35%, #775abf 96.98%);
|
||||
--ch-linear1: linear-gradient(
|
||||
112deg,
|
||||
#ac52f4 0.73%,
|
||||
#a9139a 33.95%,
|
||||
#5a409c 104.31%
|
||||
);
|
||||
|
||||
--dp-font-family: $mainFontFamily;
|
||||
}
|
11
src/app/styles/utils/vars/other.scss
Normal file
11
src/app/styles/utils/vars/other.scss
Normal file
@ -0,0 +1,11 @@
|
||||
$borderRadius2: toRem(2);
|
||||
$borderRadius4: toRem(4);
|
||||
$borderRadius6: toRem(6);
|
||||
$borderRadius8: toRem(8);
|
||||
$borderRadius10: toRem(10);
|
||||
$borderRadius12: toRem(12);
|
||||
$borderRadius16: toRem(16);
|
||||
$borderRadius20: toRem(20);
|
||||
$borderRadius24: toRem(24);
|
||||
|
||||
$widthSideBar: toRem(260);
|
0
src/app/styles/utils/vars/spaces.scss
Normal file
0
src/app/styles/utils/vars/spaces.scss
Normal file
38
src/app/styles/utils/vars/typography.scss
Normal file
38
src/app/styles/utils/vars/typography.scss
Normal file
@ -0,0 +1,38 @@
|
||||
$mainFontFamily: 'NeueHaasUnicaW1G';
|
||||
$decorationFontFamily: 'Tinkoff Sans';
|
||||
$icon: 'icon', sans-serif;
|
||||
|
||||
$tags: (
|
||||
h2: (
|
||||
toRem(28),
|
||||
normal,
|
||||
),
|
||||
h3: (
|
||||
toRem(20),
|
||||
normal,
|
||||
),
|
||||
b-16: (
|
||||
toRem(16),
|
||||
normal,
|
||||
),
|
||||
b-15: (
|
||||
toRem(15),
|
||||
normal,
|
||||
),
|
||||
b-14: (
|
||||
toRem(14),
|
||||
normal,
|
||||
),
|
||||
s-13: (
|
||||
toRem(13),
|
||||
normal,
|
||||
),
|
||||
s-12: (
|
||||
toRem(12),
|
||||
toRem(16.5),
|
||||
),
|
||||
s-11: (
|
||||
toRem(11),
|
||||
toRem(16),
|
||||
),
|
||||
);
|
18
src/entities/entities.scss
Normal file
18
src/entities/entities.scss
Normal file
@ -0,0 +1,18 @@
|
||||
/*---------------- User Styles ------------------------*/
|
||||
@import 'user/ui/UserAvatar/UserAvatar';
|
||||
|
||||
/*---------------- Patient Styles ---------------------*/
|
||||
@import 'patient/ui/PatientRequest/PatientRequest';
|
||||
@import 'patient/ui/ProgressBar/ProgressBar';
|
||||
@import 'patient/ui/PatientSurveyCard/PatientSurveyCard';
|
||||
@import 'patient/ui/PatientHealthMatrix/PatientHealthMatrix';
|
||||
@import 'patient/ui/PatientBasicInfo/PatientBasicInfo';
|
||||
@import 'patient/ui/PatientFilesCard/PatientFilesCard';
|
||||
@import 'patient/ui/PatientReminders/PatientReminders';
|
||||
@import 'patient/ui/EditableCard/EditableCard';
|
||||
@import 'patient/ui/InitialAppointment/InitialAppointment';
|
||||
@import 'patient/ui/QuestionnaireCard/QuestionnaireCard';
|
||||
@import 'patient/ui/EmptySurvey/EmptySurvey';
|
||||
@import 'patient/ui/InitialHealthMatrix/InitialHealthMatrix';
|
||||
@import 'patient/ui/InitialPurpose/InitialPurpose';
|
||||
@import 'patient/ui/EditableInput/EditableInput';
|
3
src/entities/index.ts
Normal file
3
src/entities/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './user'
|
||||
export * from './patient'
|
||||
export * from './medical'
|
245
src/entities/medical/api/index.ts
Normal file
245
src/entities/medical/api/index.ts
Normal file
@ -0,0 +1,245 @@
|
||||
import type { AxiosPromise } from 'axios'
|
||||
import type {
|
||||
Patient,
|
||||
PatientReminder,
|
||||
THealthMatrix,
|
||||
HealthMatrixData,
|
||||
PatientAnalysis,
|
||||
PatientTreatmentCourse,
|
||||
} from '@/entities'
|
||||
import { medicalApi } from '@/shared'
|
||||
import type {
|
||||
Medical,
|
||||
SetSurveyData,
|
||||
AddReminderData,
|
||||
TreatmentCourse,
|
||||
AddTreatmentCourseData,
|
||||
DeleteAppointmentData,
|
||||
DeleteTreatmentCourseFromPatientData,
|
||||
MedicalSurvey,
|
||||
ViewSurveyAnswersData,
|
||||
SurveyDetail,
|
||||
MedicalTest,
|
||||
AddOrUpdateOptimumData,
|
||||
MarkerOptimums,
|
||||
AddOrUpdateAnalysisData,
|
||||
TreatmentCourseData,
|
||||
} from '../lib'
|
||||
|
||||
/**------------------ Surveys -------------------------- */
|
||||
export const fetchSurveys = (
|
||||
search?: MedicalAPI.GET.FetchSurveys.Params,
|
||||
): MedicalAPI.GET.FetchSurveys.Response =>
|
||||
medicalApi.get('survey', {
|
||||
params: { search },
|
||||
})
|
||||
|
||||
export const setSurveysToPatient = (
|
||||
data: MedicalAPI.PUT.SetSurveysToPatient.Params,
|
||||
): MedicalAPI.PUT.SetSurveysToPatient.Response =>
|
||||
medicalApi.put(`customer/${data.customer_id}/survey`, {
|
||||
survey_ids: data.survey_ids,
|
||||
})
|
||||
|
||||
export const fetchSurveyQuestions = (
|
||||
params: MedicalAPI.GET.FetchQuestionsOfSurvey.Params,
|
||||
): MedicalAPI.GET.FetchQuestionsOfSurvey.Response =>
|
||||
medicalApi.get(`survey/${params}`)
|
||||
|
||||
export const viewSurveyAnswers = (
|
||||
params: MedicalAPI.GET.FetchAnswersOfSurvey.Params,
|
||||
): MedicalAPI.GET.FetchAnswersOfSurvey.Response =>
|
||||
medicalApi.get(
|
||||
`customer/${params.customer_id}/survey/${params.survey_attemp_id}`,
|
||||
)
|
||||
|
||||
/**------------------ Reminder -------------------------- */
|
||||
export const addReminderToPatient = (
|
||||
data: MedicalAPI.POST.addReminderToPatient.Params,
|
||||
): MedicalAPI.POST.addReminderToPatient.Response =>
|
||||
medicalApi.post('eventReminder', data)
|
||||
|
||||
/**------------------ Treatmen Course -------------------------- */
|
||||
export const fetchTreatmentCourse = (
|
||||
search?: MedicalAPI.GET.FetchTreatmentCourse.Params,
|
||||
): MedicalAPI.GET.FetchTreatmentCourse.Response =>
|
||||
medicalApi.get('treatmentCourse', {
|
||||
params: { search },
|
||||
})
|
||||
|
||||
export const addTreatmentCourseToPatient = (
|
||||
data: MedicalAPI.POST.AddTreatmentCourseToPatient.Params,
|
||||
): MedicalAPI.POST.AddTreatmentCourseToPatient.Response =>
|
||||
medicalApi.post(`users/${data.user_id}/treatmentCourseUser`, data)
|
||||
|
||||
export const addFileToTreatment = (data: {
|
||||
user_id: number
|
||||
payload: FormData
|
||||
}): MedicalAPI.POST.AddTreatmentCourseToPatient.Response =>
|
||||
medicalApi.post(`users/${data.user_id}/treatmentCourseUser`, data.payload)
|
||||
|
||||
export const fetchPatientTreatmentCourse = (
|
||||
data: MedicalAPI.GET.FetchPatientTreatmentCourse.Params,
|
||||
): MedicalAPI.GET.FetchPatientTreatmentCourse.Response =>
|
||||
medicalApi.get(`users/${data}/treatmentCourseUser`)
|
||||
|
||||
export const deleteTreatmentCourseFromPatient = (
|
||||
data: MedicalAPI.DELETE.DeleteTreatmentCourseFromPatient.Params,
|
||||
): MedicalAPI.DELETE.DeleteTreatmentCourseFromPatient.Response =>
|
||||
medicalApi.delete(
|
||||
`users/${data.user_id}/treatmentCourseUser/${data.treatment_course_id}`,
|
||||
)
|
||||
|
||||
export const editTreatmentCourse = (
|
||||
data: MedicalAPI.PUT.UpdateTreatmentCourse.Params,
|
||||
): MedicalAPI.PUT.UpdateTreatmentCourse.Response =>
|
||||
medicalApi.put(
|
||||
`users/${data.user_id}/treatmentCourseUser/${data.treatment_course_id}`,
|
||||
data.payload,
|
||||
)
|
||||
|
||||
/**------------------ Appointment -------------------------- */
|
||||
|
||||
export const deleteAppointmentFromPatient = (
|
||||
data: MedicalAPI.DELETE.DeleteAppointmentFromPatient.Params,
|
||||
): MedicalAPI.DELETE.DeleteAppointmentFromPatient.Response =>
|
||||
medicalApi.delete(`appointment/${data.appointment}`)
|
||||
|
||||
/**------------------ Medical Test -------------------------- */
|
||||
|
||||
export const fetchMedicalTests =
|
||||
(): MedicalAPI.GET.FetchAllMedicalTest.Response =>
|
||||
medicalApi.get('listMedicalTest')
|
||||
|
||||
export const updateCustomOptimum = (
|
||||
data: MedicalAPI.POST.UpdateCustomOptimum.Params,
|
||||
): MedicalAPI.POST.UpdateCustomOptimum.Response =>
|
||||
medicalApi.post('optimalCustom', data)
|
||||
|
||||
export const addOrUpdateAnalysis = (
|
||||
data: MedicalAPI.POST.AddOrUpdateAnalysis.Params,
|
||||
): MedicalAPI.POST.AddOrUpdateAnalysis.Response =>
|
||||
medicalApi.post(`users/${data.user_id}/analysis`, data)
|
||||
|
||||
/**------------------ Health Matrix -------------------------- */
|
||||
export const updateHealthMatrixValue = (
|
||||
data: MedicalAPI.POST.UpdateHealthMatrixValue.Params,
|
||||
): MedicalAPI.POST.UpdateHealthMatrixValue.Response =>
|
||||
medicalApi.post(`appointment/${data.appointment_id}/healthMatrix`, data)
|
||||
|
||||
export namespace MedicalAPI {
|
||||
export namespace GET {
|
||||
export namespace FetchSurveys {
|
||||
export type Params = string
|
||||
export type Response = AxiosPromise<{
|
||||
data: Medical['survey_list']
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace FetchTreatmentCourse {
|
||||
export type Params = string
|
||||
export type Response = AxiosPromise<{
|
||||
data: TreatmentCourse[]
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace FetchPatientTreatmentCourse {
|
||||
export type Params = Patient['id']
|
||||
export type Response = AxiosPromise<{
|
||||
data: TreatmentCourse[]
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace FetchAnswersOfSurvey {
|
||||
export type Params = ViewSurveyAnswersData
|
||||
export type Response = AxiosPromise<{
|
||||
data: SurveyDetail
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace FetchQuestionsOfSurvey {
|
||||
export type Params = MedicalSurvey['id']
|
||||
export type Response = AxiosPromise<{
|
||||
data: SurveyDetail
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace FetchAllMedicalTest {
|
||||
export type Response = AxiosPromise<{
|
||||
data: MedicalTest[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PUT {
|
||||
export namespace SetSurveysToPatient {
|
||||
export type Params = SetSurveyData
|
||||
export type Response = AxiosPromise<{
|
||||
data: any
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace UpdateTreatmentCourse {
|
||||
export type Params = TreatmentCourseData
|
||||
export type Response = AxiosPromise<{
|
||||
data: PatientTreatmentCourse
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace POST {
|
||||
export namespace addReminderToPatient {
|
||||
export type Params = AddReminderData
|
||||
export type Response = AxiosPromise<{
|
||||
data: PatientReminder
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace AddTreatmentCourseToPatient {
|
||||
export type Params = AddTreatmentCourseData
|
||||
export type Response = AxiosPromise<{
|
||||
data: any
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace UpdateCustomOptimum {
|
||||
export type Params = AddOrUpdateOptimumData
|
||||
export type Response = AxiosPromise<{
|
||||
data: MarkerOptimums
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace AddOrUpdateAnalysis {
|
||||
export type Params = AddOrUpdateAnalysisData
|
||||
export type Response = AxiosPromise<{
|
||||
data: PatientAnalysis
|
||||
}>
|
||||
}
|
||||
export namespace UpdateHealthMatrixValue {
|
||||
export type Params = HealthMatrixData
|
||||
export type Response = AxiosPromise<{
|
||||
data: Maybe<THealthMatrix>
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DELETE {
|
||||
export namespace DeleteAppointmentFromPatient {
|
||||
export type Params = DeleteAppointmentData
|
||||
export type Response = AxiosPromise<{
|
||||
data: {
|
||||
success: boolean
|
||||
data: boolean
|
||||
message: null | string
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace DeleteTreatmentCourseFromPatient {
|
||||
export type Params = DeleteTreatmentCourseFromPatientData
|
||||
export type Response = AxiosPromise<{
|
||||
data: any
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
3
src/entities/medical/index.ts
Normal file
3
src/entities/medical/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './api'
|
||||
export * from './lib'
|
||||
export * from './model'
|
44
src/entities/medical/lib/helpers.ts
Normal file
44
src/entities/medical/lib/helpers.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { MedicalTest, BaseAnalysisOptimumValues } from './types'
|
||||
|
||||
export function setTestOptimums(
|
||||
list: MedicalTest[],
|
||||
patientInfo: {
|
||||
age: number
|
||||
sex: string
|
||||
},
|
||||
): BaseAnalysisOptimumValues[] {
|
||||
return list?.map(x => {
|
||||
const markesLength = x.markers?.length || 0
|
||||
let optimums
|
||||
let i
|
||||
markers: for (i = 0; i < markesLength; i++) {
|
||||
optimums = x.markers[i].optimums?.find(y => {
|
||||
const ages = y.age.split('-').map(Number)
|
||||
if (ages.length == 2) {
|
||||
return (
|
||||
ages[0] <= patientInfo.age &&
|
||||
patientInfo.age <= ages[1] &&
|
||||
y.sex == patientInfo.sex
|
||||
)
|
||||
} else if (ages.length == 1) {
|
||||
return (
|
||||
ages[0] <= patientInfo.age && y.sex == patientInfo.sex
|
||||
)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (optimums && optimums.age) {
|
||||
break markers
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'base',
|
||||
test_id: Number(x.id),
|
||||
sex: patientInfo.sex,
|
||||
value: String(optimums?.age),
|
||||
}
|
||||
})
|
||||
}
|
2
src/entities/medical/lib/index.ts
Normal file
2
src/entities/medical/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './helpers'
|
215
src/entities/medical/lib/types.ts
Normal file
215
src/entities/medical/lib/types.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import type { Appointments, Patient, PatientAnalysis } from '@/entities'
|
||||
|
||||
export type Medical = {
|
||||
survey_list: MedicalSurvey[]
|
||||
treatment_courses: TreatmentCourse[]
|
||||
patient_treatment: TreatmentCourse[]
|
||||
survey: Maybe<SurveyDetail>
|
||||
medical_test: MedicalTest[]
|
||||
}
|
||||
|
||||
export type MedicalSurvey = {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
questions_count: number | string
|
||||
}
|
||||
|
||||
export type SetSurveyData = {
|
||||
customer_id: number
|
||||
survey_ids: number[]
|
||||
}
|
||||
|
||||
export type AddReminderData = {
|
||||
performer_id: number // user id
|
||||
datetime: string
|
||||
type: 'appointment' | 'notice'
|
||||
text: string
|
||||
}
|
||||
|
||||
export type TreatmentCourse = {
|
||||
id: number
|
||||
title: string
|
||||
duration: number
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
nutrition: string | null
|
||||
medication: string | null
|
||||
buds: string | null
|
||||
analysis_and_research: string | null
|
||||
comment: string | null
|
||||
enable: 1 | 0
|
||||
}
|
||||
|
||||
export type SurveyDetail = {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
questions: {
|
||||
id: number
|
||||
question_text: string
|
||||
question_type: 'text' | 'checkbox' | 'radio'
|
||||
survey_id: number
|
||||
options: {
|
||||
id: number
|
||||
option: string
|
||||
question_id: number
|
||||
sort: null
|
||||
is_selected?: boolean
|
||||
model?: any
|
||||
}[]
|
||||
answers?: {
|
||||
id: number
|
||||
question_id: number
|
||||
survey_attempt_id: number
|
||||
user_id: number
|
||||
answer_text: string
|
||||
question_option_id: number | null
|
||||
}[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export type MedicalTest = {
|
||||
id: number
|
||||
title: string
|
||||
created_at: null | string
|
||||
updated_at: null | string
|
||||
unit: null | string
|
||||
markers: {
|
||||
id: number
|
||||
name: string
|
||||
list_medical_test_id: number
|
||||
unit: null | string
|
||||
created_at: null | string
|
||||
updated_at: null | string
|
||||
tip_min: null | string
|
||||
tip_max: null | string
|
||||
notice: null | string
|
||||
optimums: MarkerOptimums[]
|
||||
optimums_custom: MarkerOptimums[]
|
||||
head: string[]
|
||||
result: any[]
|
||||
}[]
|
||||
}
|
||||
|
||||
// For displaying analysis
|
||||
export type TestMarkers = {
|
||||
id: number
|
||||
name: string
|
||||
list_medical_test_id: number
|
||||
unit: null | string
|
||||
created_at: null | string
|
||||
updated_at: null | string
|
||||
tip_min: null | string
|
||||
tip_max: null | string
|
||||
notice: null | string
|
||||
optimums: MarkerOptimums | MarkerOptimums[]
|
||||
optimums_custom: any[]
|
||||
result?: PatientAnalysis[]
|
||||
}
|
||||
|
||||
export type MarkerOptimums = {
|
||||
id: number
|
||||
marker_id: number
|
||||
sex: string
|
||||
age: string
|
||||
min: number
|
||||
max: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
interval?: string
|
||||
}
|
||||
|
||||
export type BaseAnalysisOptimumValues = {
|
||||
type: 'base' | 'custom'
|
||||
test_id: MedicalTest['id']
|
||||
sex: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export type AddTreatmentCourseData = {
|
||||
user_id: number
|
||||
appointment_id: number
|
||||
treatment_course_id?: number
|
||||
enabled?: boolean | number
|
||||
file?: File
|
||||
}
|
||||
|
||||
export type DeleteAppointmentData = {
|
||||
appointment: Appointments['id']
|
||||
}
|
||||
|
||||
export type DeleteTreatmentCourseFromPatientData = {
|
||||
user_id: Patient['id']
|
||||
treatment_course_id: TreatmentCourse['id']
|
||||
}
|
||||
|
||||
export type ViewSurveyAnswersData = {
|
||||
customer_id: Patient['id']
|
||||
survey_attemp_id: MedicalSurvey['id']
|
||||
}
|
||||
|
||||
export type AddOrUpdateOptimumData = {
|
||||
list_medical_test_id: number
|
||||
marker_id: number
|
||||
sex: string
|
||||
age: string
|
||||
min: number
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type AddOrUpdateAnalysisData = {
|
||||
list_medical_test_id: number
|
||||
result?: number | null
|
||||
quality: string
|
||||
date: string
|
||||
user_id: number
|
||||
marker_id: number
|
||||
}
|
||||
|
||||
export type HealthMatrixData = {
|
||||
appointment_id: number
|
||||
antecedents?: string | null
|
||||
triggers?: string | null
|
||||
medmators?: string | null
|
||||
nutrition?: string | null
|
||||
sleep?: string | null
|
||||
movement?: string | null
|
||||
stress?: string | null
|
||||
relation?: string | null
|
||||
assimilation?: string | null
|
||||
assimilation_color?: string | null
|
||||
energy?: string | null
|
||||
energy_color?: string | null
|
||||
inflammation?: string | null
|
||||
inflammation_color?: string | null
|
||||
structure?: string | null
|
||||
structure_color?: string | null
|
||||
mental?: string | null
|
||||
mental_color?: string | null
|
||||
communications?: string | null
|
||||
communications_color?: string | null
|
||||
transport?: string | null
|
||||
transport_color?: string | null
|
||||
detoxification?: string | null
|
||||
detoxification_color?: string | null
|
||||
circle_mental?: string | null
|
||||
circle_spiritual?: string | null
|
||||
circle_emotional?: string | null
|
||||
}
|
||||
|
||||
export type TreatmentCourseData = {
|
||||
user_id: Patient['id']
|
||||
treatment_course_id?: TreatmentCourse['id']
|
||||
payload?:
|
||||
| {
|
||||
nutrition?: TreatmentCourse['nutrition']
|
||||
medication?: TreatmentCourse['medication']
|
||||
buds?: TreatmentCourse['buds']
|
||||
analysis_and_research?: TreatmentCourse['analysis_and_research']
|
||||
comment?: TreatmentCourse['comment']
|
||||
title?: TreatmentCourse['title']
|
||||
enable?: TreatmentCourse['enable']
|
||||
}
|
||||
| FormData
|
||||
}
|
1
src/entities/medical/model/index.ts
Normal file
1
src/entities/medical/model/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './medical'
|
489
src/entities/medical/model/medical.ts
Normal file
489
src/entities/medical/model/medical.ts
Normal file
@ -0,0 +1,489 @@
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { toast } from 'vue3-toastify'
|
||||
import {
|
||||
addReminderToPatient,
|
||||
addTreatmentCourseToPatient,
|
||||
fetchMedicalTests,
|
||||
editTreatmentCourse,
|
||||
fetchPatientTreatmentCourse,
|
||||
fetchSurveyQuestions,
|
||||
fetchSurveys,
|
||||
fetchTreatmentCourse,
|
||||
setSurveysToPatient,
|
||||
setTestOptimums,
|
||||
updateCustomOptimum,
|
||||
viewSurveyAnswers,
|
||||
type PatientAnalysis,
|
||||
addFileToTreatment,
|
||||
} from '@/entities'
|
||||
import { usePatientStore } from '@/entities'
|
||||
import { Stores, declension } from '@/shared'
|
||||
import type {
|
||||
Medical,
|
||||
SetSurveyData,
|
||||
AddReminderData,
|
||||
AddTreatmentCourseData,
|
||||
ViewSurveyAnswersData,
|
||||
BaseAnalysisOptimumValues,
|
||||
MarkerOptimums,
|
||||
AddOrUpdateOptimumData,
|
||||
TreatmentCourseData,
|
||||
} from '../lib'
|
||||
|
||||
type MedicalState = BaseState<Maybe<Medical>>
|
||||
|
||||
export const useMedicalStore = defineStore(Stores.MEDICAL, () => {
|
||||
/**
|
||||
* State
|
||||
*/
|
||||
|
||||
const state: MedicalState = reactive({
|
||||
data: {
|
||||
survey_list: [],
|
||||
treatment_courses: [],
|
||||
patient_treatment: [],
|
||||
survey: null,
|
||||
medical_test: [],
|
||||
},
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const currOptimums = ref<BaseAnalysisOptimumValues[]>([])
|
||||
const { analysisResults, infoForMedicalTest } = storeToRefs(
|
||||
usePatientStore(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
const surveyList = computed(() => {
|
||||
return state.data?.survey_list?.map(x => ({
|
||||
...x,
|
||||
questions_count: declension(Number(x.questions_count), [
|
||||
'вопрос',
|
||||
'вопроса',
|
||||
'вопросов',
|
||||
]),
|
||||
}))
|
||||
})
|
||||
|
||||
const treatmentCourses = computed(
|
||||
() =>
|
||||
state.data?.treatment_courses?.map(x => ({
|
||||
...x,
|
||||
duration: declension(x.duration, ['день', 'дня', 'дней']),
|
||||
})),
|
||||
)
|
||||
|
||||
const patientTreatments = computed(() => {
|
||||
return (
|
||||
state.data?.patient_treatment?.map(x => ({
|
||||
id: x.id,
|
||||
title: x.title,
|
||||
})) || []
|
||||
)
|
||||
})
|
||||
|
||||
const currSurvey = computed(() => state.data?.survey)
|
||||
|
||||
const medicalTestList = computed(() => {
|
||||
if (state.data?.medical_test && state.data.medical_test?.length) {
|
||||
const newList: any[] = []
|
||||
|
||||
state.data.medical_test.forEach((x, idx) => {
|
||||
const date: Set<string> = new Set()
|
||||
const markers: any[] = []
|
||||
|
||||
x.markers?.forEach(el => {
|
||||
const result: PatientAnalysis[] = []
|
||||
let optimums: MarkerOptimums | undefined
|
||||
let interval: string = ''
|
||||
|
||||
if (currOptimums.value[idx]['type'] == 'custom') {
|
||||
optimums = el.optimums_custom?.find(x => {
|
||||
return (
|
||||
String(
|
||||
currOptimums.value[idx]['value'],
|
||||
).includes(x.age) &&
|
||||
currOptimums.value[idx].sex == x.sex
|
||||
)
|
||||
})
|
||||
} else {
|
||||
optimums = el.optimums?.find(x => {
|
||||
return (
|
||||
String(
|
||||
currOptimums.value[idx]['value'],
|
||||
).includes(x.age) &&
|
||||
currOptimums.value[idx].sex == x.sex
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
analysisResults.value?.forEach(a => {
|
||||
if (a.marker_id === el.id) {
|
||||
let analysisState: string = ''
|
||||
|
||||
if (a.result) {
|
||||
if (
|
||||
optimums?.min &&
|
||||
optimums?.max &&
|
||||
!Number.isNaN(optimums?.min) &&
|
||||
!Number.isNaN(optimums?.max)
|
||||
) {
|
||||
if (optimums.min > a.result) {
|
||||
analysisState = 'down'
|
||||
} else if (
|
||||
optimums.min <= a.result &&
|
||||
a.result <= optimums.max
|
||||
) {
|
||||
analysisState = 'normal'
|
||||
} else {
|
||||
analysisState = 'up'
|
||||
}
|
||||
|
||||
interval = `${
|
||||
optimums?.min + '-' + optimums?.max
|
||||
}`
|
||||
} else if (optimums?.min) {
|
||||
if (optimums.min > a.result) {
|
||||
analysisState = 'down'
|
||||
} else if (optimums.min == a.result) {
|
||||
analysisState = 'normal'
|
||||
} else {
|
||||
analysisState = 'up'
|
||||
}
|
||||
interval = `${optimums?.min}`
|
||||
}
|
||||
}
|
||||
|
||||
date.add(a.date)
|
||||
|
||||
result.push({
|
||||
...a,
|
||||
state: analysisState,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
markers.push({
|
||||
id: el.id,
|
||||
name: el.name,
|
||||
unit: el.unit,
|
||||
list_medical_test_id: el.list_medical_test_id,
|
||||
tip_min: el.tip_min,
|
||||
tip_max: el.tip_max,
|
||||
notice: el.notice,
|
||||
result,
|
||||
optimums: {
|
||||
...optimums,
|
||||
interval,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
newList.push({
|
||||
id: x.id,
|
||||
title: x.title,
|
||||
unit: x.unit,
|
||||
analyze_date: [...date],
|
||||
markers,
|
||||
})
|
||||
})
|
||||
|
||||
return newList
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const hasCustomGetters = (testId: number, markerId: number, age: string) =>
|
||||
computed(() => {
|
||||
const medicalTest = state.data?.medical_test.find(
|
||||
x => x.id == testId,
|
||||
)
|
||||
const testMarker = medicalTest?.markers.find(x => x.id == markerId)
|
||||
|
||||
return testMarker?.optimums_custom.find(x => x.age == age)
|
||||
})
|
||||
|
||||
const updateCurrOptimumsVal = (optimum: {
|
||||
value: string
|
||||
sex: string
|
||||
type: 'base' | 'custom'
|
||||
test_id: number
|
||||
}) => {
|
||||
currOptimums.value.forEach((el, idx) => {
|
||||
if (el.test_id == optimum.test_id) {
|
||||
currOptimums.value[idx] = {
|
||||
...optimum,
|
||||
sex:
|
||||
optimum.sex == 'children'
|
||||
? infoForMedicalTest.value.sex
|
||||
: optimum.sex,
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
const setSurveyList = async (search: string) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await fetchSurveys(search)
|
||||
|
||||
if (data.data && state.data?.survey_list) {
|
||||
state.data.survey_list = data.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const setTreatmentCourseList = async (search: string) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await fetchTreatmentCourse(search)
|
||||
if (data.data?.length && state.data?.treatment_courses) {
|
||||
state.data.treatment_courses = data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e ->', e)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const setPatientTreatmentCourseList = async (payload: number) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await fetchPatientTreatmentCourse(payload)
|
||||
|
||||
if (data.data?.length && state.data?.patient_treatment) {
|
||||
state.data.patient_treatment = data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e ->', e)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const addDestinationToPatient = async (payload: AddTreatmentCourseData) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await addTreatmentCourseToPatient(payload)
|
||||
if (data.data) {
|
||||
usePatientStore().setDataToState(data.data)
|
||||
}
|
||||
|
||||
toast.success('Успешно сохранено!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const addFileToTreatmentCourse = async (payload: any) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await addFileToTreatment(payload)
|
||||
if (data.data) {
|
||||
usePatientStore().setDataToState(data.data)
|
||||
}
|
||||
|
||||
toast.success('Успешно сохранено!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const addSurveyToPatient = async (payload: SetSurveyData) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await setSurveysToPatient(payload)
|
||||
usePatientStore().setDataToState(data.data)
|
||||
toast.success('Успешно сохранено!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const addReminder = async (payload: AddReminderData) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await addReminderToPatient(payload)
|
||||
|
||||
usePatientStore().setReminderToState(data.data)
|
||||
toast.success('Успешно сохранено!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const viewSurvey = async (survey_id: number) => {
|
||||
state.loading = true
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await fetchSurveyQuestions(survey_id)
|
||||
if (data.data && state.data) {
|
||||
state.data.survey = data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const viewSurveyResult = async (payload: ViewSurveyAnswersData) => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await viewSurveyAnswers(payload)
|
||||
if (data.data && state.data) {
|
||||
state.data.survey = data.data
|
||||
|
||||
state.data.survey['questions'] = data.data.questions.map(x => ({
|
||||
...x,
|
||||
options: x.options.map(y => ({
|
||||
...y,
|
||||
model: y.is_selected ? y.id : '',
|
||||
})),
|
||||
}))
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const setMedicalTest = async () => {
|
||||
try {
|
||||
state.loading = true
|
||||
const { data } = await fetchMedicalTests()
|
||||
if (data.data && data.data?.length && state.data?.medical_test) {
|
||||
currOptimums.value = setTestOptimums(
|
||||
data.data,
|
||||
infoForMedicalTest.value,
|
||||
)
|
||||
state.data.medical_test = data.data
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e ->', e)
|
||||
}
|
||||
|
||||
state.loading = true
|
||||
}
|
||||
|
||||
/**
|
||||
* только для кастомных оптимумов
|
||||
* @param {AddOrUpdateOptimumData} payload
|
||||
*/
|
||||
const addOrUpdateCustomOptimum = async (
|
||||
payload: AddOrUpdateOptimumData,
|
||||
) => {
|
||||
try {
|
||||
const { data } = await updateCustomOptimum(payload)
|
||||
toast.success('Успешно сохранено!')
|
||||
|
||||
if (data.data && data.data?.marker_id) {
|
||||
const testIdx = state.data?.medical_test.length || 0
|
||||
let markerIdx, optimumIdx, i, j
|
||||
testLoop: for (i = 0; i < testIdx; i++) {
|
||||
if (
|
||||
state.data?.medical_test[i]['id'] ==
|
||||
payload.list_medical_test_id
|
||||
) {
|
||||
markerIdx =
|
||||
state.data?.medical_test[i]?.markers?.length || 0
|
||||
for (j = 0; j < markerIdx; j++) {
|
||||
if (
|
||||
state.data.medical_test[i]['markers'][j][
|
||||
'id'
|
||||
] == payload.marker_id
|
||||
) {
|
||||
optimumIdx =
|
||||
state.data.medical_test[i]['markers'][j]?.[
|
||||
'optimums_custom'
|
||||
]?.length || 0
|
||||
let hasOptimum = false
|
||||
for (let k = 0; k < optimumIdx; k++) {
|
||||
if (
|
||||
state.data.medical_test[i]['markers'][
|
||||
j
|
||||
]?.['optimums_custom'][k]['sex'] ==
|
||||
payload.sex &&
|
||||
state.data.medical_test[i]['markers'][
|
||||
j
|
||||
]?.['optimums_custom'][k]['age'] ==
|
||||
payload.age
|
||||
) {
|
||||
state.data.medical_test[i]['markers'][
|
||||
j
|
||||
]['optimums_custom'][k] = {
|
||||
...data.data,
|
||||
}
|
||||
hasOptimum = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasOptimum) {
|
||||
state.data.medical_test[i]['markers'][j][
|
||||
'optimums_custom'
|
||||
].push({
|
||||
...data.data,
|
||||
})
|
||||
|
||||
break testLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e ->', e)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTreatmentCourse = async (payload: TreatmentCourseData) => {
|
||||
try {
|
||||
const { data } = await editTreatmentCourse(payload)
|
||||
|
||||
if (data.data) {
|
||||
usePatientStore().setTreatmentDataToState(data.data)
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
surveyList,
|
||||
currSurvey,
|
||||
treatmentCourses,
|
||||
patientTreatments,
|
||||
medicalTestList,
|
||||
currOptimums,
|
||||
hasCustomGetters,
|
||||
updateCurrOptimumsVal,
|
||||
setSurveyList,
|
||||
addSurveyToPatient,
|
||||
addReminder,
|
||||
setTreatmentCourseList,
|
||||
addDestinationToPatient,
|
||||
setPatientTreatmentCourseList,
|
||||
viewSurvey,
|
||||
viewSurveyResult,
|
||||
setMedicalTest,
|
||||
addOrUpdateCustomOptimum,
|
||||
updateTreatmentCourse,
|
||||
addFileToTreatmentCourse,
|
||||
}
|
||||
})
|
0
src/entities/medical/ui/index.ts
Normal file
0
src/entities/medical/ui/index.ts
Normal file
180
src/entities/patient/api/index.ts
Normal file
180
src/entities/patient/api/index.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import { type AxiosPromise } from 'axios'
|
||||
import { baseApi, medicalApi } from '@/shared'
|
||||
import type {
|
||||
Appointments,
|
||||
EditAppointmentData,
|
||||
EditPatientData,
|
||||
Patient,
|
||||
PatientAnalysis,
|
||||
PatientMediaFile,
|
||||
} from '../lib'
|
||||
|
||||
/**------------------ Patient -------------------------- */
|
||||
export const fetchPatients = async ({
|
||||
search,
|
||||
page,
|
||||
per_page,
|
||||
}: PatientAPI.GET.FetchPatients.Params): PatientAPI.GET.FetchPatients.Response => {
|
||||
const response = await baseApi.get(
|
||||
`customer?search=${search}&page=${page}&perPage=${per_page}`,
|
||||
)
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const fetchPatient = async (
|
||||
data: PatientAPI.GET.FetchPatient.Params,
|
||||
): PatientAPI.GET.FetchPatient.Response => {
|
||||
const response = await baseApi.get(`customer/${data}`)
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const deletePatient = (data: PatientAPI.DELETE.DeletePatient.Params) =>
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
console.log('delete: ', data)
|
||||
|
||||
resolve(data)
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
export const editPatient = (
|
||||
data: PatientAPI.PUT.EditPatient.Params,
|
||||
): PatientAPI.PUT.EditPatient.Response =>
|
||||
baseApi.put(`customer/${data.id}`, data)
|
||||
|
||||
/**------------------ Patient Appointment -------------------------- */
|
||||
export const editPatientAppointment = (
|
||||
data: PatientAPI.PUT.EditAppointment.Params,
|
||||
): PatientAPI.PUT.EditAppointment.Response =>
|
||||
medicalApi.put(`appointment/${data.id}`, data)
|
||||
|
||||
export const addAppointmentToPatient = (
|
||||
data: PatientAPI.POST.AddAppointment.Params,
|
||||
): PatientAPI.POST.AddAppointment.Response =>
|
||||
medicalApi.post('appointment', data)
|
||||
|
||||
/**------------------ Set Avatar of Customer -------------------------- */
|
||||
export const setPatientAvatar = (
|
||||
data: PatientAPI.POST.SetAvatar.Params,
|
||||
): PatientAPI.POST.SetAvatar.Response => baseApi.post('user/setAvatar', data)
|
||||
|
||||
/**------------------ Media Files -------------------------- */
|
||||
export const setFilesToPatient = (
|
||||
data: PatientAPI.POST.SetFiles.Params,
|
||||
): PatientAPI.POST.SetFiles.Response =>
|
||||
medicalApi.post(`customer/${data.customer_id}/file`, data.files)
|
||||
|
||||
export const deleteMediaFile = async (
|
||||
data: PatientAPI.DELETE.DeleteFile.Params,
|
||||
) => medicalApi.delete(`file/${data}`)
|
||||
|
||||
/**------------------ Medical Tests -------------------------- */
|
||||
|
||||
export const fetchCustomerMedicalTest = (
|
||||
customer_id: PatientAPI.GET.FetchCustomerMedicalTests.Params,
|
||||
): PatientAPI.GET.FetchCustomerMedicalTests.Response =>
|
||||
medicalApi.get(`users/${customer_id}/analysis`)
|
||||
|
||||
/**
|
||||
* Module PatientAPI
|
||||
* Interfaces:
|
||||
* EditPatientParams
|
||||
* EditAppointmentParams
|
||||
*/
|
||||
interface EditPatientParams extends EditPatientData {
|
||||
id: number
|
||||
}
|
||||
|
||||
interface EditAppointmentParams extends EditAppointmentData {
|
||||
id: Appointments['id']
|
||||
user_id: Appointments['user_id']
|
||||
}
|
||||
export namespace PatientAPI {
|
||||
export namespace GET {
|
||||
export namespace FetchPatients {
|
||||
export type Params = {
|
||||
page: number
|
||||
per_page: number
|
||||
search: string
|
||||
}
|
||||
export type Response = AxiosPromise<PaginationData<Patient[]>>
|
||||
}
|
||||
|
||||
export namespace FetchPatient {
|
||||
export type Params = Patient['id']
|
||||
export type Response = AxiosPromise<Patient>
|
||||
|
||||
// export type Response = AxiosResponse<{
|
||||
// data: Patient[]
|
||||
// }>
|
||||
}
|
||||
|
||||
export namespace FetchCustomerMedicalTests {
|
||||
export type Params = Patient['id']
|
||||
export type Response = AxiosPromise<{
|
||||
data: PatientAnalysis[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DELETE {
|
||||
export namespace DeletePatient {
|
||||
export type Params = Patient['id']
|
||||
}
|
||||
|
||||
export namespace DeleteFile {
|
||||
export type Params = PatientMediaFile['id']
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PUT {
|
||||
export namespace EditPatient {
|
||||
export type Params = EditPatientParams
|
||||
export type Response = AxiosPromise<{
|
||||
data: Patient
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace EditAppointment {
|
||||
export type Params = EditAppointmentParams
|
||||
export type Response = AxiosPromise<{
|
||||
data: Appointments
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export namespace POST {
|
||||
export namespace AddAppointment {
|
||||
export type Params = {
|
||||
user_id: Appointments['user_id']
|
||||
}
|
||||
export type Response = AxiosPromise<{
|
||||
data: {
|
||||
user_id: number
|
||||
updated_at: string
|
||||
created_at: string
|
||||
id: number
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace SetAvatar {
|
||||
export type Params = FormData
|
||||
export type Response = AxiosPromise<{
|
||||
data: Patient
|
||||
}>
|
||||
}
|
||||
|
||||
export namespace SetFiles {
|
||||
export type Params = {
|
||||
customer_id: Patient['id']
|
||||
files: FormData
|
||||
}
|
||||
export type Response = AxiosPromise<{
|
||||
data: Patient
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
4
src/entities/patient/index.ts
Normal file
4
src/entities/patient/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './api'
|
||||
export * from './ui'
|
||||
export * from './lib'
|
||||
export * from './model'
|
65
src/entities/patient/lib/hooks.ts
Normal file
65
src/entities/patient/lib/hooks.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { fetchPatients } from '../api'
|
||||
import type { Patient, PatientsState, PatientTableRow } from '../lib'
|
||||
|
||||
export const useSearchPatient = (cb: (search: string) => void) => {
|
||||
let timeout: Timeout
|
||||
|
||||
const search = ref('')
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
|
||||
watch(search, value => {
|
||||
clearTimeout(timeout)
|
||||
|
||||
timeout = setTimeout(async () => {
|
||||
await cb(value)
|
||||
}, 300)
|
||||
})
|
||||
|
||||
return search
|
||||
}
|
||||
|
||||
export const useFetchPatients = async (
|
||||
search: string = '',
|
||||
page: Pagination['current_page'] = 1,
|
||||
state: PatientsState,
|
||||
converter: (list: Patient[]) => PatientTableRow[],
|
||||
) => {
|
||||
const { data } = await fetchPatients({
|
||||
page,
|
||||
per_page: state.pagination.per_page,
|
||||
search,
|
||||
})
|
||||
|
||||
const newData = converter(data.data)
|
||||
|
||||
return {
|
||||
pagination: data,
|
||||
data: !state.data || search ? newData : [...state.data, ...newData],
|
||||
}
|
||||
}
|
||||
|
||||
export const useBasePatientsLoad = (
|
||||
cb: (search?: string, page?: Pagination['current_page']) => Promise<void>,
|
||||
currentPage: Pagination['current_page'],
|
||||
lastPage: Pagination['last_page'],
|
||||
isFirstLoad: boolean,
|
||||
) => {
|
||||
const loadData = async ($state: any) => {
|
||||
await cb('', currentPage + 1)
|
||||
if (currentPage < lastPage) {
|
||||
$state.loaded()
|
||||
} else {
|
||||
$state.complete()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (isFirstLoad) await cb()
|
||||
})
|
||||
|
||||
return loadData
|
||||
}
|
2
src/entities/patient/lib/index.ts
Normal file
2
src/entities/patient/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './hooks'
|
205
src/entities/patient/lib/types.ts
Normal file
205
src/entities/patient/lib/types.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import type { LinkProps, UserBaseProps } from '@/shared'
|
||||
import type { PatientRequestProps } from '../ui'
|
||||
|
||||
export type Patient = {
|
||||
id: number
|
||||
sex: string
|
||||
avatar: string
|
||||
name: string
|
||||
gender: Gender
|
||||
anamnesis: string | null
|
||||
asking: string | null
|
||||
birthdate: string
|
||||
children_count: number
|
||||
city: string
|
||||
contact: string
|
||||
email: string
|
||||
marital: string
|
||||
profession: string
|
||||
media: PatientMediaFile[]
|
||||
medical_test: PatientMedicalTest[]
|
||||
survey_attempts: PatientSurvey[]
|
||||
files: PatientFiles[]
|
||||
health_matrix: number[]
|
||||
appointments: Appointments[]
|
||||
event_reminder_customer: PatientReminder[]
|
||||
}
|
||||
|
||||
export type PatientTableRow = {
|
||||
id: Patient['id']
|
||||
patient: UserBaseProps
|
||||
age: number
|
||||
gender: Gender
|
||||
contact?: LinkProps
|
||||
time?: string
|
||||
actions?: boolean
|
||||
type?: string
|
||||
description?: string
|
||||
request?: PatientRequestProps['request']
|
||||
applicationDate?: string
|
||||
reminder?: string
|
||||
}
|
||||
|
||||
export type PatientsState = BaseStatePagination<Maybe<PatientTableRow[]>>
|
||||
|
||||
export enum PatientStep {
|
||||
MAIN,
|
||||
QUESTIONNAIRE,
|
||||
ANALYZES,
|
||||
FILES,
|
||||
HEALTH_MATRIX,
|
||||
PURPOSE,
|
||||
}
|
||||
|
||||
export type PatientFiles = {
|
||||
id: number
|
||||
name: string
|
||||
file_name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type PatientSurvey = {
|
||||
id: number
|
||||
title: string
|
||||
survey_id: number
|
||||
attemp: number
|
||||
percent: number
|
||||
answers_count: number
|
||||
survey: {
|
||||
id: number
|
||||
title: string
|
||||
questions_count: number
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PatientMediaFile = {
|
||||
id: number
|
||||
file_name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type PatientMedicalTest = {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
unit: string | null
|
||||
medical_tests: any[]
|
||||
}
|
||||
|
||||
export type Appointments = {
|
||||
id: number
|
||||
user_id: number
|
||||
complaint: string
|
||||
taking_medication: string
|
||||
taking_bud: string
|
||||
physical_activity: string
|
||||
stress: string
|
||||
sleep: string
|
||||
bad_habits: string
|
||||
conclusion: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
treatment_course_user?: PatientTreatmentCourse
|
||||
health_matrix: THealthMatrix
|
||||
}
|
||||
|
||||
export type THealthMatrix = {
|
||||
id: number
|
||||
appointment_id: number
|
||||
antecedents: string | null
|
||||
triggers: string | null
|
||||
medmators: string | null
|
||||
nutrition: string | null
|
||||
sleep: string | null
|
||||
movement: string | null
|
||||
stress: string | null
|
||||
relation: string | null
|
||||
assimilation: string | null
|
||||
assimilation_color: string | null
|
||||
energy: string | null
|
||||
energy_color: string | null
|
||||
inflammation: string | null
|
||||
inflammation_color: string | null
|
||||
structure: string | null
|
||||
structure_color: string | null
|
||||
mental: string | null
|
||||
mental_color: string | null
|
||||
communications: string | null
|
||||
communications_color: string | null
|
||||
transport: string | null
|
||||
transport_color: string | null
|
||||
detoxification: string | null
|
||||
detoxification_color: string | null
|
||||
circle_mental: string | null
|
||||
circle_spiritual: string | null
|
||||
circle_emotional: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type EditPatientData = {
|
||||
name?: string
|
||||
sex?: string
|
||||
city?: string
|
||||
asking?: string
|
||||
marital?: string
|
||||
contact?: string
|
||||
anamnesis?: string
|
||||
birthdate?: string
|
||||
profession?: string
|
||||
children_count?: string
|
||||
}
|
||||
|
||||
export type EditAppointmentData = {
|
||||
complaint?: Appointments['complaint']
|
||||
taking_medication?: Appointments['taking_medication']
|
||||
taking_bud?: Appointments['taking_bud']
|
||||
physical_activity?: Appointments['physical_activity']
|
||||
stress?: Appointments['stress']
|
||||
sleep?: Appointments['sleep']
|
||||
bad_habits?: Appointments['bad_habits']
|
||||
conclusion?: Appointments['conclusion']
|
||||
}
|
||||
|
||||
export type PatientReminder = {
|
||||
id: number
|
||||
text: string
|
||||
type: string
|
||||
user_id: number
|
||||
performer_id: number
|
||||
datetime: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type PatientTreatmentCourse = {
|
||||
id: number
|
||||
title: string
|
||||
duration: number | null
|
||||
user_id: number
|
||||
performer_id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
appointment_id: number
|
||||
nutrition: string | null
|
||||
medication: string
|
||||
buds: string | null
|
||||
analysis_and_research: string | null
|
||||
comment: string | null
|
||||
media: PatientFiles[]
|
||||
enabled: number
|
||||
}
|
||||
|
||||
export type PatientAnalysis = {
|
||||
id?: number
|
||||
user_id: number
|
||||
performer_id?: number
|
||||
result: number | null
|
||||
marker_id: number
|
||||
quality: string
|
||||
date: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
state?: string
|
||||
}
|
2
src/entities/patient/model/converters/index.ts
Normal file
2
src/entities/patient/model/converters/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './patientsToMyPatients'
|
||||
export * from './patientsToRequestsPatients'
|
@ -0,0 +1,20 @@
|
||||
import { dateToAge } from '@/shared'
|
||||
import type { Patient, PatientTableRow } from '../../lib'
|
||||
|
||||
export const patientsToMyPatients = (list: Patient[]): PatientTableRow[] => {
|
||||
return list.map(item => ({
|
||||
id: item.id,
|
||||
patient: {
|
||||
name: item.name || '',
|
||||
avatar: item.avatar || '',
|
||||
},
|
||||
gender: 1,
|
||||
age: dateToAge(item.birthdate),
|
||||
request: item.asking || '',
|
||||
reminder: 'моковые данные, с бека не приходит!!!',
|
||||
contact: {
|
||||
href: `tel:${item.contact || ''}`,
|
||||
text: item.contact || '',
|
||||
},
|
||||
}))
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { dateToAge } from '@/shared'
|
||||
import type { Patient, PatientTableRow } from '../../lib'
|
||||
|
||||
export const patientsToRequestsPatients = (
|
||||
list: Patient[],
|
||||
): PatientTableRow[] => {
|
||||
return list.map(item => ({
|
||||
id: item.id,
|
||||
patient: {
|
||||
name: item.name || '',
|
||||
avatar: item.avatar || '',
|
||||
},
|
||||
gender: 1,
|
||||
age: dateToAge(item.birthdate),
|
||||
applicationDate: '2023-10-23T05:57:37.000000Z',
|
||||
request: item.asking || '',
|
||||
actions: true,
|
||||
}))
|
||||
}
|
1
src/entities/patient/model/index.ts
Normal file
1
src/entities/patient/model/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './module'
|
3
src/entities/patient/model/module/index.ts
Normal file
3
src/entities/patient/model/module/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './my-patients'
|
||||
export * from './request-patients'
|
||||
export * from './patient'
|
54
src/entities/patient/model/module/my-patients.ts
Normal file
54
src/entities/patient/model/module/my-patients.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
import { patientsToMyPatients } from '@/entities/patient/model/converters'
|
||||
import { Stores } from '@/shared'
|
||||
import { deletePatient } from '../../api'
|
||||
import type { Patient, PatientsState } from '../../lib'
|
||||
import { useFetchPatients } from '../../lib'
|
||||
|
||||
export const usePatientsStore = defineStore(Stores.MY_PATIENTS, () => {
|
||||
const state = reactive<PatientsState>({
|
||||
loading: false,
|
||||
data: null,
|
||||
pagination: {
|
||||
current_page: 1,
|
||||
per_page: 10,
|
||||
last_page: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const setMyPatients = async (
|
||||
search: string = '',
|
||||
page: Pagination['current_page'] = 1,
|
||||
) => {
|
||||
try {
|
||||
const { data, pagination } = await useFetchPatients(
|
||||
search,
|
||||
page,
|
||||
state,
|
||||
patientsToMyPatients,
|
||||
)
|
||||
|
||||
state.pagination = pagination
|
||||
state.data = data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMyPatient = async (id: Patient['id']) => {
|
||||
try {
|
||||
await deletePatient(id)
|
||||
|
||||
state.data = state.data?.filter(item => item.id !== id)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
setMyPatients: setMyPatients,
|
||||
deleteMyPatient: deleteMyPatient,
|
||||
}
|
||||
})
|
562
src/entities/patient/model/module/patient.ts
Normal file
562
src/entities/patient/model/module/patient.ts
Normal file
@ -0,0 +1,562 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { toast } from 'vue3-toastify'
|
||||
import {
|
||||
editPatient,
|
||||
fetchPatient,
|
||||
editPatientAppointment,
|
||||
addAppointmentToPatient,
|
||||
setPatientAvatar,
|
||||
setFilesToPatient,
|
||||
deleteMediaFile,
|
||||
deleteAppointmentFromPatient,
|
||||
deleteTreatmentCourseFromPatient,
|
||||
updateHealthMatrixValue,
|
||||
type DeleteTreatmentCourseFromPatientData,
|
||||
fetchCustomerMedicalTest,
|
||||
addOrUpdateAnalysis,
|
||||
type AddOrUpdateAnalysisData,
|
||||
type HealthMatrixData,
|
||||
} from '@/entities'
|
||||
import {
|
||||
Stores,
|
||||
dateToAge,
|
||||
declension,
|
||||
formattingDateForClient,
|
||||
getTimeFromDate,
|
||||
prettifyDate,
|
||||
} from '@/shared'
|
||||
import {
|
||||
PatientStep,
|
||||
type Appointments,
|
||||
type EditAppointmentData,
|
||||
type EditPatientData,
|
||||
type Patient,
|
||||
type PatientReminder,
|
||||
type PatientMediaFile,
|
||||
type PatientAnalysis,
|
||||
type PatientTreatmentCourse,
|
||||
} from '../../lib'
|
||||
|
||||
type PatientState = BaseState<Maybe<Patient>>
|
||||
export const APPOINTMENTSITEMS: {
|
||||
key:
|
||||
| 'taking_medication'
|
||||
| 'taking_bud'
|
||||
| 'physical_activity'
|
||||
| 'stress'
|
||||
| 'sleep'
|
||||
| 'bad_habits'
|
||||
| 'complaint'
|
||||
name: any
|
||||
}[] = [
|
||||
{
|
||||
name: 'Прием медикаментов',
|
||||
key: 'taking_medication',
|
||||
},
|
||||
{
|
||||
name: 'Прием бадов',
|
||||
key: 'taking_bud',
|
||||
},
|
||||
{
|
||||
name: 'Физическая активность',
|
||||
key: 'physical_activity',
|
||||
},
|
||||
{
|
||||
name: 'Стресс',
|
||||
key: 'stress',
|
||||
},
|
||||
{
|
||||
name: 'Сон',
|
||||
key: 'sleep',
|
||||
},
|
||||
{
|
||||
name: 'Вредные привычки',
|
||||
key: 'bad_habits',
|
||||
},
|
||||
{
|
||||
name: 'Жалобы',
|
||||
key: 'complaint',
|
||||
},
|
||||
]
|
||||
|
||||
export const usePatientStore = defineStore(Stores.PATIENT, () => {
|
||||
/**-------- State -------------- */
|
||||
|
||||
const state = reactive<PatientState>({
|
||||
loading: false,
|
||||
data: null,
|
||||
})
|
||||
|
||||
const analysisResults = ref<PatientAnalysis[]>([])
|
||||
const idxAppointment = ref(0)
|
||||
const patientStep = ref<PatientStep>(PatientStep.MAIN)
|
||||
|
||||
/**-------- Action -------------- */
|
||||
const setCurrentPatient = async (id: Patient['id']) => {
|
||||
try {
|
||||
state.loading = true
|
||||
|
||||
const { data } = await fetchPatient(id)
|
||||
state.data = data
|
||||
idxAppointment.value = data?.appointments?.[0]?.id || 0
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetCurrentPatient = () => {
|
||||
state.data = null
|
||||
}
|
||||
|
||||
const onEditPatient = async (payload: EditPatientData) => {
|
||||
try {
|
||||
const { data } = await editPatient({
|
||||
...payload,
|
||||
id: Number(state.data?.id),
|
||||
})
|
||||
state.data = data.data
|
||||
toast.success('Изменения сохранены !')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
const onEditAppointment = async (payload: EditAppointmentData) => {
|
||||
try {
|
||||
const { data } = await editPatientAppointment({
|
||||
...payload,
|
||||
id: Number(currAppointment.value?.id),
|
||||
user_id: Number(currAppointment.value?.user_id),
|
||||
})
|
||||
const appointment = data.data
|
||||
let appointmentID = -1
|
||||
state.data?.appointments.forEach((el, i) => {
|
||||
if (el.id == appointment.id) {
|
||||
appointmentID = i
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof appointmentID == 'number' && appointmentID > -1) {
|
||||
APPOINTMENTSITEMS.forEach(elem => {
|
||||
if (state.data?.appointments[appointmentID]) {
|
||||
state.data.appointments[appointmentID][elem.key] =
|
||||
appointment[elem.key]
|
||||
}
|
||||
})
|
||||
}
|
||||
toast.success('Изменения сохранены!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onCreateAppointment = async (payload: {
|
||||
user_id: Appointments['user_id']
|
||||
}) => {
|
||||
try {
|
||||
await addAppointmentToPatient(payload)
|
||||
await setCurrentPatient(payload.user_id)
|
||||
toast.success('Успешно добавлено новый прием!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateAvatar = async (payload: FormData) => {
|
||||
try {
|
||||
const { data } = await setPatientAvatar(payload)
|
||||
if (data.data) {
|
||||
state.data = data.data
|
||||
}
|
||||
toast.success('Изменения сохранены')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onUploadFiles = async (payload: FormData) => {
|
||||
try {
|
||||
const { data } = await setFilesToPatient({
|
||||
customer_id: Number(state.data?.id),
|
||||
files: payload,
|
||||
})
|
||||
|
||||
if (data.data) {
|
||||
state.data = data.data
|
||||
}
|
||||
toast.success('Файлы успешно сохранены')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteMediaFile = async (payload: {
|
||||
id: PatientMediaFile['id']
|
||||
type: 'patient' | 'treatmentCourse'
|
||||
}) => {
|
||||
try {
|
||||
state.loading = true
|
||||
|
||||
await deleteMediaFile(payload.id)
|
||||
|
||||
if (payload.type == 'patient' && state.data?.media) {
|
||||
const filteredMedia = state.data.media.filter(
|
||||
file => file.id !== payload.id,
|
||||
)
|
||||
|
||||
state.data.media = filteredMedia
|
||||
}
|
||||
|
||||
if (payload.type == 'treatmentCourse') {
|
||||
state.data?.appointments?.forEach((app, idx) => {
|
||||
if (
|
||||
app.id == currAppointment.value?.id &&
|
||||
state.data?.appointments[idx]?.treatment_course_user
|
||||
) {
|
||||
const media =
|
||||
state.data.appointments[
|
||||
idx
|
||||
].treatment_course_user?.media.filter(
|
||||
x => x.id != payload.id,
|
||||
) || []
|
||||
|
||||
state.data.appointments[idx].treatment_course_user = {
|
||||
analysis_and_research:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.analysis_and_research || '',
|
||||
appointment_id:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.appointment_id || 0,
|
||||
buds:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.buds || '[]',
|
||||
comment:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.comment || '',
|
||||
created_at:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.created_at || '',
|
||||
duration:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.duration || 0,
|
||||
enabled:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.enabled || 0,
|
||||
id:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.id || 1,
|
||||
media: media,
|
||||
medication:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.medication || '[]',
|
||||
nutrition:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.nutrition || '',
|
||||
performer_id:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.performer_id || 1,
|
||||
title:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.title || '',
|
||||
updated_at:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.updated_at || '',
|
||||
user_id:
|
||||
currAppointment.value.treatment_course_user
|
||||
?.user_id || 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toast.success('Файл удален')
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
||||
toast.error('Произошла ошибка')
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteAppointment = async (id: Appointments['id']) => {
|
||||
try {
|
||||
await deleteAppointmentFromPatient({ appointment: id })
|
||||
|
||||
if (state.data?.appointments) {
|
||||
state.data.appointments = state.data?.appointments.filter(
|
||||
x => x.id != id,
|
||||
)
|
||||
}
|
||||
if (idxAppointment.value == id) {
|
||||
idxAppointment.value = state.data?.appointments?.[0]?.id || 0
|
||||
}
|
||||
toast.success('Прием удален')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteTreatmentCourse = async (
|
||||
payload: DeleteTreatmentCourseFromPatientData,
|
||||
) => {
|
||||
try {
|
||||
await deleteTreatmentCourseFromPatient(payload)
|
||||
state.data?.appointments?.forEach((app, idx) => {
|
||||
if (
|
||||
app.id == currAppointment.value?.id &&
|
||||
state.data?.appointments[idx]?.treatment_course_user
|
||||
) {
|
||||
state.data.appointments[idx].treatment_course_user =
|
||||
undefined
|
||||
}
|
||||
})
|
||||
|
||||
toast.success('Назначения удален')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const setPatientAnalysis = async () => {
|
||||
try {
|
||||
if (state.data?.id) {
|
||||
const { data } = await fetchCustomerMedicalTest(state.data.id)
|
||||
|
||||
if (data.data && Array.isArray(data.data)) {
|
||||
analysisResults.value = data.data
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const updatePatientAnalysisResult = async (
|
||||
payload: AddOrUpdateAnalysisData,
|
||||
) => {
|
||||
try {
|
||||
const { data } = await addOrUpdateAnalysis(payload)
|
||||
const lenAnalysis = analysisResults.value.length
|
||||
let isChanged = false
|
||||
|
||||
for (let i = 0; i < lenAnalysis; i++) {
|
||||
if (
|
||||
analysisResults.value[i]['marker_id'] ==
|
||||
payload.marker_id &&
|
||||
analysisResults.value[i]['date'] == payload.date
|
||||
) {
|
||||
analysisResults.value[i]['result'] = Number(
|
||||
data?.data?.result || payload.result,
|
||||
)
|
||||
isChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isChanged) {
|
||||
// If analysisResult has not been changed, then this is a new analysis
|
||||
analysisResults.value.push(data.data)
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const updateAnalysisDate = async (payload: AddOrUpdateAnalysisData) => {
|
||||
try {
|
||||
const { data } = await addOrUpdateAnalysis(payload)
|
||||
const lenAnalysis = analysisResults.value.length
|
||||
|
||||
for (let i = 0; i < lenAnalysis; i++) {
|
||||
if (
|
||||
analysisResults.value[i]['marker_id'] ==
|
||||
payload.marker_id &&
|
||||
analysisResults.value[i]['id'] == data.data.id
|
||||
) {
|
||||
analysisResults.value[i] = {
|
||||
...data.data,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const onUpdateHealthMatrix = async (payload: HealthMatrixData) => {
|
||||
try {
|
||||
const { data } = await updateHealthMatrixValue(payload)
|
||||
if (data.data?.appointment_id) {
|
||||
for (let i = 0; i < appointmentLen.value; i++) {
|
||||
if (
|
||||
state.data?.appointments[i]?.health_matrix
|
||||
?.appointment_id == data.data.appointment_id
|
||||
) {
|
||||
state.data.appointments[i].health_matrix = {
|
||||
...data.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
toast.success('Изменения сохранены!')
|
||||
} catch (e: any) {
|
||||
toast.error(`Что-то не так! ${e?.message || ''}`)
|
||||
console.log('e -> ', e)
|
||||
}
|
||||
}
|
||||
|
||||
const setDataToState = (data: Patient) => {
|
||||
state.data = data
|
||||
}
|
||||
|
||||
const setReminderToState = (data: PatientReminder) => {
|
||||
state.data?.event_reminder_customer.push(data)
|
||||
}
|
||||
|
||||
const setTreatmentDataToState = (data: PatientTreatmentCourse) => {
|
||||
state.data?.appointments?.forEach((app, idx) => {
|
||||
if (
|
||||
app.id == currAppointment.value?.id &&
|
||||
state.data?.appointments[idx]
|
||||
) {
|
||||
state.data.appointments[idx].treatment_course_user = data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**-------- Getters -------------- */
|
||||
const appointmentLen = computed(() => state.data?.appointments?.length || 0)
|
||||
const patientInfo = computed(() => ({
|
||||
sex: state.data?.sex || '',
|
||||
name: state.data?.name || '',
|
||||
city: state.data?.city || '',
|
||||
avatar: state.data?.avatar || '',
|
||||
marital: state.data?.marital || '',
|
||||
contact: state.data?.contact || '',
|
||||
birthdate: state.data?.birthdate || '',
|
||||
profession: state.data?.profession || '',
|
||||
children_count: state.data?.children_count || '',
|
||||
birthday: prettifyDate(state.data?.birthdate) || '',
|
||||
age:
|
||||
declension(dateToAge(state.data?.birthdate || '0'), [
|
||||
'год',
|
||||
'года',
|
||||
'лет',
|
||||
]) || '',
|
||||
asking: state.data?.asking || '',
|
||||
anamnesis: state.data?.anamnesis || '',
|
||||
}))
|
||||
|
||||
const analyzes = computed(
|
||||
() =>
|
||||
state.data?.medical_test?.map(x => ({
|
||||
...x,
|
||||
created_at: prettifyDate(x.created_at),
|
||||
})) || [],
|
||||
)
|
||||
|
||||
const survey = computed(
|
||||
() =>
|
||||
state.data?.survey_attempts?.map(x => ({
|
||||
id: x.id,
|
||||
title: x.survey?.title,
|
||||
total: x.survey?.questions_count || 0,
|
||||
answers: x.answers_count || 0,
|
||||
percent: x.percent,
|
||||
})),
|
||||
)
|
||||
|
||||
const files = computed(() => state.data?.files)
|
||||
|
||||
const media = computed(() => state.data?.media)
|
||||
|
||||
const matrixHealth = computed(() => state.data?.health_matrix)
|
||||
|
||||
const reminders = computed(
|
||||
() =>
|
||||
state.data?.event_reminder_customer?.map(x => ({
|
||||
id: x.id,
|
||||
date: String(
|
||||
formattingDateForClient(x.datetime, 'short'),
|
||||
).replace(/\./g, ''),
|
||||
time: getTimeFromDate(x.datetime),
|
||||
type: x.type,
|
||||
name: x.text,
|
||||
})),
|
||||
)
|
||||
|
||||
const appointments = computed(
|
||||
() =>
|
||||
state.data?.appointments?.map(x => ({
|
||||
id: x.id,
|
||||
name: prettifyDate(x.created_at) || '',
|
||||
})),
|
||||
)
|
||||
|
||||
const currAppointment = computed(
|
||||
() => state.data?.appointments?.find(x => x.id == idxAppointment.value),
|
||||
)
|
||||
|
||||
const infoForMedicalTest = computed(
|
||||
(): {
|
||||
sex: string
|
||||
age: number
|
||||
} => ({
|
||||
sex: state.data?.sex || '',
|
||||
age: dateToAge(state.data?.birthdate || '0'),
|
||||
}),
|
||||
)
|
||||
|
||||
const treatmentCourse = computed((): PatientTreatmentCourse | null => {
|
||||
return currAppointment.value?.treatment_course_user || null
|
||||
})
|
||||
|
||||
return {
|
||||
state,
|
||||
media,
|
||||
files,
|
||||
survey,
|
||||
analyzes,
|
||||
reminders,
|
||||
patientInfo,
|
||||
patientStep,
|
||||
matrixHealth,
|
||||
appointments,
|
||||
idxAppointment,
|
||||
currAppointment,
|
||||
analysisResults,
|
||||
infoForMedicalTest,
|
||||
treatmentCourse,
|
||||
setCurrentPatient,
|
||||
resetCurrentPatient,
|
||||
onEditPatient,
|
||||
onEditAppointment,
|
||||
onCreateAppointment,
|
||||
setDataToState,
|
||||
setReminderToState,
|
||||
setTreatmentDataToState,
|
||||
onUpdateAvatar,
|
||||
onUploadFiles,
|
||||
onDeleteMediaFile,
|
||||
onDeleteAppointment,
|
||||
onDeleteTreatmentCourse,
|
||||
setPatientAnalysis,
|
||||
updatePatientAnalysisResult,
|
||||
updateAnalysisDate,
|
||||
onUpdateHealthMatrix,
|
||||
}
|
||||
})
|
44
src/entities/patient/model/module/request-patients.ts
Normal file
44
src/entities/patient/model/module/request-patients.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
import { Stores } from '@/shared'
|
||||
import { type PatientsState, useFetchPatients } from '../../lib'
|
||||
import { patientsToRequestsPatients } from '../converters'
|
||||
|
||||
export const useRequestPatientsStore = defineStore(
|
||||
Stores.REQUEST_PATIENTS,
|
||||
() => {
|
||||
const state = reactive<PatientsState>({
|
||||
loading: false,
|
||||
data: null,
|
||||
pagination: {
|
||||
current_page: 1,
|
||||
per_page: 10,
|
||||
last_page: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const setRequestPatients = async (
|
||||
search: string = '',
|
||||
page: Pagination['current_page'] = 1,
|
||||
) => {
|
||||
try {
|
||||
const { data, pagination } = await useFetchPatients(
|
||||
search,
|
||||
page,
|
||||
state,
|
||||
patientsToRequestsPatients,
|
||||
)
|
||||
|
||||
state.pagination = pagination
|
||||
state.data = data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
setRequestPatients,
|
||||
}
|
||||
},
|
||||
)
|
58
src/entities/patient/ui/EditableCard/EditableCard.scss
Normal file
58
src/entities/patient/ui/EditableCard/EditableCard.scss
Normal file
@ -0,0 +1,58 @@
|
||||
.editable__card {
|
||||
&--title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(24);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: $mainFontFamily;
|
||||
color: var(--dark-main);
|
||||
@include fontSize(
|
||||
s-13,
|
||||
(
|
||||
line-height: 1.3,
|
||||
)
|
||||
);
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--dark-64);
|
||||
@include fontSize(
|
||||
s-13,
|
||||
(
|
||||
line-height: 1.3,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
textarea {
|
||||
border: none;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
@include fontSize(
|
||||
s-13,
|
||||
(
|
||||
line-height: 1.3,
|
||||
)
|
||||
);
|
||||
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
resize: none;
|
||||
color: var(--dark-main);
|
||||
}
|
||||
}
|
92
src/entities/patient/ui/EditableCard/EditableCard.vue
Normal file
92
src/entities/patient/ui/EditableCard/EditableCard.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div
|
||||
:class="bem('card')"
|
||||
@focus="cardFocusEvent"
|
||||
@blur="cardBlurEvent"
|
||||
tabindex="0"
|
||||
>
|
||||
<card-component rounded hoverable size="m" :active="isFocused">
|
||||
<template #header>
|
||||
<h4 :class="bem('card--title')">{{ title }}</h4>
|
||||
</template>
|
||||
<template v-if="isFocused">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="data"
|
||||
@blur="textareaBlurEvent"
|
||||
@focus="autoGrow"
|
||||
@input="autoGrow"
|
||||
>
|
||||
</textarea>
|
||||
</template>
|
||||
<template v-else>
|
||||
<pre v-if="value">{{ value }}</pre>
|
||||
<p v-else>Не заполнено</p>
|
||||
</template>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="Editable">
|
||||
import { ref, nextTick, watch } from 'vue'
|
||||
|
||||
export type EditableCardProps = {
|
||||
value: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const props = defineProps<EditableCardProps>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', val: string): void
|
||||
}>()
|
||||
|
||||
const isFocused = ref<boolean>(false)
|
||||
const data = ref<string>(props.value)
|
||||
const textarea = ref<HTMLElement>()
|
||||
|
||||
watch(props, () => {
|
||||
data.value = props.value
|
||||
})
|
||||
|
||||
/**------------- Methods --------------- */
|
||||
|
||||
const cardFocusEvent = (e: FocusEvent) => {
|
||||
const target = e.relatedTarget as HTMLElement
|
||||
if (isFocused.value && target?.tagName == 'TEXTAREA') {
|
||||
return
|
||||
}
|
||||
isFocused.value = true
|
||||
|
||||
nextTick(() => {
|
||||
const textarea = document.getElementsByTagName('textarea')?.[0]
|
||||
textarea?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const cardBlurEvent = (e: FocusEvent) => {
|
||||
const target = e.relatedTarget as HTMLElement
|
||||
if (target?.tagName !== 'TEXTAREA') {
|
||||
isFocused.value = false
|
||||
if (props.value != data.value) emit('save', data.value)
|
||||
}
|
||||
}
|
||||
|
||||
const textareaBlurEvent = (e: FocusEvent) => {
|
||||
const target = e.relatedTarget as HTMLElement
|
||||
if (target?.className !== 'patient-asking__card') {
|
||||
isFocused.value = false
|
||||
if (props.value != data.value) emit('save', data.value)
|
||||
}
|
||||
}
|
||||
|
||||
const autoGrow = () => {
|
||||
if (textarea.value) {
|
||||
const height = textarea.value.scrollHeight
|
||||
if (!data.value) {
|
||||
textarea.value.style.height = '17px'
|
||||
} else {
|
||||
textarea.value.style.height = 'auto'
|
||||
textarea.value.style.height = height + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
3
src/entities/patient/ui/EditableCard/index.ts
Normal file
3
src/entities/patient/ui/EditableCard/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import EditableCard, { type EditableCardProps } from './EditableCard.vue'
|
||||
|
||||
export { EditableCard, type EditableCardProps }
|
20
src/entities/patient/ui/EditableInput/EditableInput.scss
Normal file
20
src/entities/patient/ui/EditableInput/EditableInput.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.editable-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
p {
|
||||
@include fontSize(s-13);
|
||||
cursor: pointer;
|
||||
padding: toRem(8) 0;
|
||||
&.empty {
|
||||
text-align: center;
|
||||
color: var(--dark-32);
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: toRem(6);
|
||||
border: 1px solid var(--brand-main);
|
||||
border-radius: $borderRadius6;
|
||||
}
|
||||
}
|
43
src/entities/patient/ui/EditableInput/EditableInput.vue
Normal file
43
src/entities/patient/ui/EditableInput/EditableInput.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div :class="bem()" tabindex="0" @focus="focusInput">
|
||||
<template v-if="isFocused">
|
||||
<input type="text" v-model="val" @blur="blurInput" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-if="modelValue">{{ modelValue }}</p>
|
||||
<p v-else class="empty">{{ placeholder }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="EditableInput">
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
export type EditableInputProps = {
|
||||
modelValue: any
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const props = defineProps<EditableInputProps>()
|
||||
|
||||
const isFocused = ref<boolean>()
|
||||
const val = ref<string>('')
|
||||
|
||||
const focusInput = () => {
|
||||
isFocused.value = true
|
||||
val.value = props.modelValue
|
||||
|
||||
nextTick(() => {
|
||||
const input = document.querySelector(
|
||||
'.editable-input input',
|
||||
) as HTMLInputElement
|
||||
|
||||
input?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const blurInput = () => {
|
||||
emit('update:modelValue', val.value)
|
||||
isFocused.value = false
|
||||
}
|
||||
</script>
|
3
src/entities/patient/ui/EditableInput/index.ts
Normal file
3
src/entities/patient/ui/EditableInput/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import EditableInput, { type EditableInputProps } from './EditableInput.vue'
|
||||
|
||||
export { EditableInput, type EditableInputProps }
|
20
src/entities/patient/ui/EmptySurvey/EmptySurvey.scss
Normal file
20
src/entities/patient/ui/EmptySurvey/EmptySurvey.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.empty-survey {
|
||||
aspect-ratio: 1 / 1.1;
|
||||
max-height: 252px;
|
||||
background: var(--brand-4-bg);
|
||||
border-radius: $borderRadius20;
|
||||
|
||||
&__title {
|
||||
@include fontSize(
|
||||
h3,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
@include fontSize(b-14);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
11
src/entities/patient/ui/EmptySurvey/EmptySurvey.vue
Normal file
11
src/entities/patient/ui/EmptySurvey/EmptySurvey.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div :class="[...bem(), 'column', 'items-center', 'justify-center']">
|
||||
<span :class="bem('title')">Не назначен ни один опросник</span>
|
||||
<span :class="bem('subtitle')">
|
||||
Выберите опросник из библиотеки справа и назначьте пациенту
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="EmptySurvey">
|
||||
export type EmptySurveyProps = {}
|
||||
</script>
|
3
src/entities/patient/ui/EmptySurvey/index.ts
Normal file
3
src/entities/patient/ui/EmptySurvey/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import EmptySurvey, { type EmptySurveyProps } from './EmptySurvey.vue'
|
||||
|
||||
export { EmptySurvey, type EmptySurveyProps }
|
@ -0,0 +1,28 @@
|
||||
.initial-appointment {
|
||||
aspect-ratio: 1 / 1.1;
|
||||
max-height: 390px;
|
||||
background: var(--brand-4-bg);
|
||||
border-radius: $borderRadius20;
|
||||
|
||||
&__title {
|
||||
@include fontSize(
|
||||
h2,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
max-width: 330px;
|
||||
@include fontSize(b-14);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
margin-top: toRem(24);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div :class="[...bem(), 'column', 'items-center', 'justify-center']">
|
||||
<span :class="bem('title')">Первичный прием</span>
|
||||
<span :class="bem('subtitle')"
|
||||
>Добавьте карточку первичного приема, чтобы начать собирать данные о
|
||||
пациенте в динамике</span
|
||||
>
|
||||
<div :class="bem('actions')">
|
||||
<button-component
|
||||
text="Создать первичный прием"
|
||||
view="flat"
|
||||
rounded
|
||||
size="m"
|
||||
@click="onCreateAppointment({ user_id })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="InitialAppointment">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePatientStore } from '@/entities'
|
||||
|
||||
export type InitialAppointmentProps = {}
|
||||
const user_id = Number(useRoute().params.id)
|
||||
|
||||
const { onCreateAppointment } = usePatientStore()
|
||||
</script>
|
5
src/entities/patient/ui/InitialAppointment/index.ts
Normal file
5
src/entities/patient/ui/InitialAppointment/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import InitialAppointment, {
|
||||
type InitialAppointmentProps,
|
||||
} from './InitialAppointment.vue'
|
||||
|
||||
export { InitialAppointment, type InitialAppointmentProps }
|
@ -0,0 +1,27 @@
|
||||
.initial-health-matrix {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1.1;
|
||||
max-height: 413px;
|
||||
background: var(--brand-4-bg);
|
||||
border-radius: $borderRadius20;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: toRem(45);
|
||||
|
||||
&__title {
|
||||
max-width: toRem(340);
|
||||
text-align: center;
|
||||
margin-bottom: toRem(24);
|
||||
|
||||
@include fontSize(
|
||||
h3,
|
||||
(
|
||||
weight: 700,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<span :class="bem('title')">
|
||||
Для добавления матрицы здоровья создайте первый прием
|
||||
</span>
|
||||
<button-component
|
||||
text="Создать прием"
|
||||
view="brand"
|
||||
rounded
|
||||
size="m"
|
||||
@click="onCreateAppointment({ user_id })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="InitialHealthMatrix">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { usePatientStore } from '@/entities'
|
||||
|
||||
export type InitialHealthMatrixProps = {}
|
||||
const user_id = Number(useRoute().params.id)
|
||||
|
||||
const { onCreateAppointment } = usePatientStore()
|
||||
</script>
|
5
src/entities/patient/ui/InitialHealthMatrix/index.ts
Normal file
5
src/entities/patient/ui/InitialHealthMatrix/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import InitialHealthMatrix, {
|
||||
type InitialHealthMatrixProps,
|
||||
} from './InitialHealthMatrix.vue'
|
||||
|
||||
export { InitialHealthMatrix, type InitialHealthMatrixProps }
|
24
src/entities/patient/ui/InitialPurpose/InitialPurpose.scss
Normal file
24
src/entities/patient/ui/InitialPurpose/InitialPurpose.scss
Normal file
@ -0,0 +1,24 @@
|
||||
.initial-purpose {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1.1;
|
||||
max-height: 413px;
|
||||
background: var(--brand-4-bg);
|
||||
border-radius: $borderRadius20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: toRem(45);
|
||||
|
||||
&__title {
|
||||
max-width: toRem(340);
|
||||
text-align: center;
|
||||
margin-bottom: toRem(24);
|
||||
@include fontSize(
|
||||
h3,
|
||||
(
|
||||
weight: 700,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
27
src/entities/patient/ui/InitialPurpose/InitialPurpose.vue
Normal file
27
src/entities/patient/ui/InitialPurpose/InitialPurpose.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<span :class="bem('title')">
|
||||
Для добавления назначения создайте первый прием
|
||||
</span>
|
||||
|
||||
<button-component
|
||||
text="Создать прием"
|
||||
view="brand"
|
||||
rounded
|
||||
size="m"
|
||||
@click="onCreateAppointment({ user_id })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" bem-block="InitialPurpose">
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { usePatientStore } from '@/entities'
|
||||
|
||||
export type InitialPurposeProps = {}
|
||||
|
||||
const user_id = Number(useRoute().params.id)
|
||||
|
||||
const { onCreateAppointment } = usePatientStore()
|
||||
</script>
|
3
src/entities/patient/ui/InitialPurpose/index.ts
Normal file
3
src/entities/patient/ui/InitialPurpose/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import InitialPurpose, { type InitialPurposeProps } from './InitialPurpose.vue'
|
||||
|
||||
export { InitialPurpose, type InitialPurposeProps }
|
@ -0,0 +1,21 @@
|
||||
.patient-basic-info {
|
||||
.row-name {
|
||||
text-align: left;
|
||||
font-size: toRem(16);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__row[class$='unavailable'] {
|
||||
color: var(--dark-32);
|
||||
}
|
||||
|
||||
table {
|
||||
td,
|
||||
th {
|
||||
font-size: toRem(13);
|
||||
line-height: toRem(20);
|
||||
padding: toRem(4) 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
133
src/entities/patient/ui/PatientBasicInfo/PatientBasicInfo.vue
Normal file
133
src/entities/patient/ui/PatientBasicInfo/PatientBasicInfo.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<div class="row justify-between items-center">
|
||||
<user-avatar
|
||||
:avatar="patientInfo.avatar"
|
||||
:user-id="userId"
|
||||
@update:avatar="updateAvatar"
|
||||
/>
|
||||
<button-component
|
||||
icon="pencil-line"
|
||||
view="secondary"
|
||||
size="m"
|
||||
@click="showEditPatientInfoModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<table>
|
||||
<tr v-for="(item, key) in data" :key="key">
|
||||
<th v-if="item.title" align="left">
|
||||
{{ item.title }}
|
||||
</th>
|
||||
<td
|
||||
align="right"
|
||||
:class="
|
||||
bem(`row row-${key}`, { unavailable: !item.value })
|
||||
"
|
||||
>
|
||||
<span v-if="item.value">{{ item.value }}</span>
|
||||
<span v-else>{{
|
||||
item?.placeholder || 'не указано'
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="PatientBasicInfo">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ModalsName, useModalsStore } from '@/widgets'
|
||||
import { usePatientStore, type EditPatientData, UserAvatar } from '@/entities'
|
||||
|
||||
export type PatientBasicInfoProps = {}
|
||||
type DataType = {
|
||||
[key: string]: {
|
||||
title: string
|
||||
value: any
|
||||
placeholder?: string
|
||||
}
|
||||
}
|
||||
|
||||
const { patientInfo } = storeToRefs(usePatientStore())
|
||||
const data = computed<DataType>(() => ({
|
||||
name: {
|
||||
title: '',
|
||||
value: patientInfo.value.name,
|
||||
placeholder: 'ФИО',
|
||||
},
|
||||
sex: {
|
||||
title: 'Пол',
|
||||
value: patientInfo.value.sex == 'male' ? 'Мужчина' : 'Женщина',
|
||||
},
|
||||
age: {
|
||||
title: 'Возраст',
|
||||
value: patientInfo.value.age,
|
||||
},
|
||||
birthday: {
|
||||
title: 'Дата рождения',
|
||||
value: patientInfo.value.birthday,
|
||||
},
|
||||
city: {
|
||||
title: 'Город проживания',
|
||||
value: patientInfo.value.city,
|
||||
},
|
||||
marital: {
|
||||
title: 'Семейное положение',
|
||||
value: patientInfo.value.marital,
|
||||
},
|
||||
children_count: {
|
||||
title: 'Кол-во детей',
|
||||
value: patientInfo.value.children_count,
|
||||
},
|
||||
profession: {
|
||||
title: 'Профессия',
|
||||
value: patientInfo.value.profession,
|
||||
},
|
||||
contact: {
|
||||
title: 'Контакты',
|
||||
value: patientInfo.value.contact,
|
||||
},
|
||||
}))
|
||||
const userId = useRoute().params.id as string
|
||||
|
||||
/**------------ Methods ------------------ */
|
||||
const { onEditPatient, onUpdateAvatar } = usePatientStore()
|
||||
const modals = useModalsStore()
|
||||
|
||||
const updateAvatar = async (formdata: FormData) => {
|
||||
await onUpdateAvatar(formdata)
|
||||
}
|
||||
|
||||
const savePatientInfo = async (data: EditPatientData) => {
|
||||
modals.closeModal()
|
||||
await onEditPatient(data)
|
||||
}
|
||||
const showEditPatientInfoModal = () => {
|
||||
modals.setModal(ModalsName.EDITPATIENT, {
|
||||
title: 'Основная информация',
|
||||
data: {
|
||||
name: patientInfo.value.name,
|
||||
sex: patientInfo.value.sex,
|
||||
city: patientInfo.value.city,
|
||||
marital: patientInfo.value.marital,
|
||||
contact: patientInfo.value.contact,
|
||||
birthdate: patientInfo.value.birthdate,
|
||||
profession: patientInfo.value.profession,
|
||||
children_count: patientInfo.value.children_count,
|
||||
},
|
||||
success: {
|
||||
text: 'Сохранить',
|
||||
cb: savePatientInfo,
|
||||
},
|
||||
cansel: {
|
||||
text: 'Отмена',
|
||||
cb: modals.closeModal,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
5
src/entities/patient/ui/PatientBasicInfo/index.ts
Normal file
5
src/entities/patient/ui/PatientBasicInfo/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientBasicInfo, {
|
||||
type PatientBasicInfoProps,
|
||||
} from './PatientBasicInfo.vue'
|
||||
|
||||
export { PatientBasicInfo, type PatientBasicInfoProps }
|
@ -0,0 +1,64 @@
|
||||
.patient-files {
|
||||
&__card {
|
||||
&--title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&--button {
|
||||
height: 20px;
|
||||
font-size: toRem(13);
|
||||
padding: 0;
|
||||
&:hover {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(20);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: toRem(13);
|
||||
color: var(--dark-64);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: toRem(10);
|
||||
|
||||
&--item {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@include fontSize(s-13);
|
||||
color: var(--dark-main) !important;
|
||||
|
||||
i {
|
||||
padding: 0 toRem(8) 0 0;
|
||||
}
|
||||
|
||||
&-name {
|
||||
width: 240px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div :class="bem('card')">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<div class="row justify-between items-center">
|
||||
<h4 :class="bem('card--title')">Файлы</h4>
|
||||
<slot name="add-file"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<ul v-if="files?.length" :class="bem('list')">
|
||||
<li
|
||||
v-for="(f, i) in files"
|
||||
:key="`file-${i}`"
|
||||
:class="bem('list--item')"
|
||||
>
|
||||
<a
|
||||
:href="f.url"
|
||||
:class="bem(`list--item file`)"
|
||||
target="_blank"
|
||||
>
|
||||
<icon-base-component name="file" size="m" />
|
||||
<span :class="bem(`list--item file-name`)">{{
|
||||
f.file_name
|
||||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p v-else>Не ни одного файлв</p>
|
||||
|
||||
<template v-if="files?.length" #actions>
|
||||
<button-component
|
||||
text="Посмотреть все"
|
||||
view="secondary"
|
||||
text-position="left"
|
||||
:class="bem('card--button')"
|
||||
@click="moveToFilesStep"
|
||||
/>
|
||||
</template>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="PatientFiles">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePatientStore, PatientStep } from '@/entities'
|
||||
|
||||
export type PatientFilesCardProps = {}
|
||||
|
||||
const { files, patientStep } = storeToRefs(usePatientStore())
|
||||
|
||||
const moveToFilesStep = () => {
|
||||
patientStep.value = PatientStep.FILES
|
||||
}
|
||||
</script>
|
5
src/entities/patient/ui/PatientFilesCard/index.ts
Normal file
5
src/entities/patient/ui/PatientFilesCard/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientFilesCard, {
|
||||
type PatientFilesCardProps,
|
||||
} from './PatientFilesCard.vue'
|
||||
|
||||
export { PatientFilesCard, type PatientFilesCardProps }
|
@ -0,0 +1,51 @@
|
||||
.health-matrix {
|
||||
&__card {
|
||||
&--title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(20);
|
||||
}
|
||||
}
|
||||
|
||||
&__graph {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&--item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:first-child .overlay {
|
||||
border-radius: toRem(24) 0 0 toRem(24);
|
||||
}
|
||||
|
||||
&:last-child .overlay {
|
||||
border-radius: 0 toRem(24) toRem(24) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
height: toRem(24);
|
||||
}
|
||||
|
||||
.value {
|
||||
display: block;
|
||||
padding: toRem(6) 0 0;
|
||||
text-align: center;
|
||||
@include fontSize(
|
||||
s-13,
|
||||
(
|
||||
line-height: toRem(20),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div v-if="matrixHealth?.length" :class="bem('card')">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<div class="row justify-between items-center">
|
||||
<h4 :class="bem('card--title')">Матрица здоровья</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div :class="bem('graph')">
|
||||
<div
|
||||
v-for="(m, i) in matrixHealth"
|
||||
:key="i"
|
||||
:class="bem('graph', 'item')"
|
||||
:style="`flex: 0 0 ${m}%;`"
|
||||
>
|
||||
<div
|
||||
class="overlay"
|
||||
:style="`background: ${getMatrixColor(i)}`"
|
||||
></div>
|
||||
<span class="value">{{ m }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<button-component
|
||||
text="Подробнее "
|
||||
view="secondary"
|
||||
size="m"
|
||||
width="88px"
|
||||
:class="bem('card--button')"
|
||||
@click="moveToHealthMatrix"
|
||||
/>
|
||||
</template>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="healthMatrix">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { usePatientStore, PatientStep } from '@/entities'
|
||||
|
||||
export type PatientHealthMatrixProps = {}
|
||||
|
||||
const colors = ['#f25c80', '#fe9b7d', '#b2d677']
|
||||
const { matrixHealth, patientStep } = storeToRefs(usePatientStore())
|
||||
const getMatrixColor = (idx: number): string => {
|
||||
if (matrixHealth.value?.[idx]) return colors?.[idx]
|
||||
|
||||
return '#' + Math.floor(Math.random() * 16777215).toString(16)
|
||||
}
|
||||
const moveToHealthMatrix = () => {
|
||||
patientStep.value = PatientStep.HEALTH_MATRIX
|
||||
}
|
||||
</script>
|
5
src/entities/patient/ui/PatientHealthMatrix/index.ts
Normal file
5
src/entities/patient/ui/PatientHealthMatrix/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientHealthMatrix, {
|
||||
type PatientHealthMatrixProps,
|
||||
} from './PatientHealthMatrix.vue'
|
||||
|
||||
export { PatientHealthMatrix, type PatientHealthMatrixProps }
|
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<tabs-component v-model="valueModel" :list="list" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" bem-block="PatientNavigation">
|
||||
import { computed } from 'vue'
|
||||
import type { TabsProps } from '@/shared'
|
||||
import { PatientStep } from '../../lib'
|
||||
|
||||
export type PatientNavigationProps = {
|
||||
modelValue: PatientStep
|
||||
}
|
||||
|
||||
type Emits = {
|
||||
(e: 'update:modelValue', value: PatientStep): PatientStep
|
||||
}
|
||||
|
||||
const props = defineProps<PatientNavigationProps>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const valueModel = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const list: TabsProps['list'] = [
|
||||
{
|
||||
id: PatientStep.MAIN,
|
||||
text: 'Карточка',
|
||||
},
|
||||
{
|
||||
id: PatientStep.QUESTIONNAIRE,
|
||||
text: 'Опросники',
|
||||
},
|
||||
{
|
||||
id: PatientStep.ANALYZES,
|
||||
text: 'Анализы',
|
||||
},
|
||||
{
|
||||
id: PatientStep.FILES,
|
||||
text: 'Файлы',
|
||||
},
|
||||
{
|
||||
id: PatientStep.HEALTH_MATRIX,
|
||||
text: 'Матрица здоровья',
|
||||
},
|
||||
{
|
||||
id: PatientStep.PURPOSE,
|
||||
text: 'Назначение',
|
||||
},
|
||||
]
|
||||
</script>
|
5
src/entities/patient/ui/PatientNavigation/index.ts
Normal file
5
src/entities/patient/ui/PatientNavigation/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientNavigation, {
|
||||
type PatientNavigationProps,
|
||||
} from './PatientNavigation.vue'
|
||||
|
||||
export { PatientNavigation, type PatientNavigationProps }
|
@ -0,0 +1,69 @@
|
||||
.patient-reminders {
|
||||
&__card {
|
||||
&--title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&--button {
|
||||
height: 20px;
|
||||
font-size: toRem(13);
|
||||
padding: 0;
|
||||
&:hover {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(20);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: toRem(13);
|
||||
color: var(--dark-64);
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
td,
|
||||
th {
|
||||
padding: toRem(8) 0;
|
||||
@include fontSize(
|
||||
s-13,
|
||||
(
|
||||
line-height: 1.5,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
th {
|
||||
padding-right: toRem(4);
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: toRem(50);
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--dark-64);
|
||||
}
|
||||
|
||||
mark {
|
||||
min-width: toRem(50);
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 toRem(4);
|
||||
color: var(--dark-main);
|
||||
background: var(--blue-20);
|
||||
border-radius: 3px;
|
||||
@include fontSize(s-13);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div :class="bem('card')">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<div class="row justify-between items-center">
|
||||
<h4 :class="bem('card--title')">Напоминания</h4>
|
||||
<button-component
|
||||
icon="plus"
|
||||
view="secondary"
|
||||
size="xs"
|
||||
@click="showAddReminderModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<table v-if="reminders?.length" :class="bem('list')">
|
||||
<tr
|
||||
v-for="(r, i) in reminders"
|
||||
:key="`reminder_${i}`"
|
||||
:class="bem('list--item')"
|
||||
>
|
||||
<th align="left">{{ r.name || '--' }}</th>
|
||||
<td align="right">
|
||||
<span>{{ r.date }}</span>
|
||||
<mark>{{ r.time }}</mark>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p v-else>Не ни одного напоминания</p>
|
||||
|
||||
<template v-if="reminders?.length" #actions>
|
||||
<button-component
|
||||
text="Посмотреть все"
|
||||
view="secondary"
|
||||
text-position="left"
|
||||
:class="bem('card--button')"
|
||||
@click="moveToPurposeStep"
|
||||
/>
|
||||
</template>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="PatientReminders">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ModalsName, useModalsStore } from '@/widgets'
|
||||
import { usePatientStore, useMedicalStore, PatientStep } from '@/entities'
|
||||
|
||||
export type PatientRemindersProps = {}
|
||||
|
||||
const { reminders, patientStep } = storeToRefs(usePatientStore())
|
||||
const modals = useModalsStore()
|
||||
const { addReminder } = useMedicalStore()
|
||||
const performerId = useRoute().params.id
|
||||
|
||||
const onAddReminder = async (data: {
|
||||
datetime: string
|
||||
type: 'appointment' | 'notice'
|
||||
text: string
|
||||
}) => {
|
||||
await addReminder({
|
||||
...data,
|
||||
performer_id: Number(performerId),
|
||||
})
|
||||
modals.closeModal()
|
||||
}
|
||||
|
||||
const showAddReminderModal = () => {
|
||||
modals.setModal(ModalsName.ADDREMINDER, {
|
||||
title: 'Создать напоминание',
|
||||
success: {
|
||||
text: 'Сохранить',
|
||||
cb: onAddReminder,
|
||||
},
|
||||
cansel: {
|
||||
text: 'Отмена',
|
||||
cb: modals.closeModal,
|
||||
},
|
||||
})
|
||||
}
|
||||
const moveToPurposeStep = () => {
|
||||
patientStep.value = PatientStep.PURPOSE
|
||||
}
|
||||
</script>
|
5
src/entities/patient/ui/PatientReminders/index.ts
Normal file
5
src/entities/patient/ui/PatientReminders/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientReminders, {
|
||||
type PatientRemindersProps,
|
||||
} from './PatientReminders.vue'
|
||||
|
||||
export { PatientReminders, type PatientRemindersProps }
|
10
src/entities/patient/ui/PatientRequest/PatientRequest.scss
Normal file
10
src/entities/patient/ui/PatientRequest/PatientRequest.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.patient-request {
|
||||
display: flex;
|
||||
gap: toRem(6);
|
||||
|
||||
&__tooltip-content {
|
||||
@include column(toRem(12));
|
||||
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
37
src/entities/patient/ui/PatientRequest/PatientRequest.vue
Normal file
37
src/entities/patient/ui/PatientRequest/PatientRequest.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<tag-component v-if="request.length" :text="request[0]" />
|
||||
<tooltip-component
|
||||
v-if="request.length > 1"
|
||||
title="Запрос"
|
||||
icon="bell"
|
||||
:class="bem('tooltip')"
|
||||
>
|
||||
<template v-slot:parent>
|
||||
<tag-component :text="countText" hoverable view="grey" />
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<div :class="bem('tooltip-content')">
|
||||
<tag-component
|
||||
v-for="text in request"
|
||||
:key="text"
|
||||
:text="text"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tooltip-component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" bem-block="patient-request">
|
||||
import { computed } from 'vue'
|
||||
|
||||
export type PatientRequestProps = {
|
||||
// FIXME change type on Request[]
|
||||
request: string
|
||||
}
|
||||
|
||||
const props = defineProps<PatientRequestProps>()
|
||||
|
||||
const countText = computed(() => `+${props.request.length - 1}`)
|
||||
</script>
|
3
src/entities/patient/ui/PatientRequest/index.ts
Normal file
3
src/entities/patient/ui/PatientRequest/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import PatientRequest, { type PatientRequestProps } from './PatientRequest.vue'
|
||||
|
||||
export { PatientRequest, type PatientRequestProps }
|
@ -0,0 +1,39 @@
|
||||
.patient-survey {
|
||||
&__card {
|
||||
&--title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&--button {
|
||||
height: 20px;
|
||||
font-size: toRem(13);
|
||||
padding: 0;
|
||||
&:hover {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(20);
|
||||
}
|
||||
|
||||
table {
|
||||
td,
|
||||
th {
|
||||
font-size: toRem(13);
|
||||
line-height: toRem(20);
|
||||
padding: toRem(4) 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: toRem(13);
|
||||
color: var(--dark-64);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div :class="bem('card')">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<div class="row justify-between items-center">
|
||||
<h4 :class="bem('card--title')">Опросники</h4>
|
||||
<button-component
|
||||
icon="plus"
|
||||
view="secondary"
|
||||
size="xs"
|
||||
@click="openSelectQuestionnaireModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<table v-if="survey?.length">
|
||||
<tr v-for="(s, i) in survey" :key="i">
|
||||
<th align="left">{{ s.title }}</th>
|
||||
<td>
|
||||
<progress-bar
|
||||
height="5"
|
||||
:percent="s.percent"
|
||||
:total="s.total"
|
||||
:answers="s.answers"
|
||||
row
|
||||
></progress-bar>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p v-else>Пока нет ни одного опросника</p>
|
||||
|
||||
<template v-if="survey?.length" #actions>
|
||||
<button-component
|
||||
text="Посмотреть все"
|
||||
view="secondary"
|
||||
text-position="left"
|
||||
:class="bem('card--button')"
|
||||
@click="moveToQuestionnaireStep"
|
||||
/>
|
||||
</template>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="PatientSurvey">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useModalsStore, ModalsName } from '@/widgets'
|
||||
import {
|
||||
usePatientStore,
|
||||
ProgressBar,
|
||||
useMedicalStore,
|
||||
PatientStep,
|
||||
} from '@/entities'
|
||||
|
||||
export type PatientSurveyCardProps = {}
|
||||
|
||||
const { survey, patientStep } = storeToRefs(usePatientStore())
|
||||
const { surveyList } = storeToRefs(useMedicalStore())
|
||||
const customerId = useRoute().params.id
|
||||
|
||||
const { setModal, closeModal } = useModalsStore()
|
||||
const { setSurveyList, addSurveyToPatient } = useMedicalStore()
|
||||
|
||||
const sendSelectQuestionnaires = async (data: any[]) => {
|
||||
await addSurveyToPatient({
|
||||
customer_id: Number(customerId),
|
||||
survey_ids: data.map(x => Number(x)),
|
||||
})
|
||||
closeModal()
|
||||
}
|
||||
|
||||
const openSelectQuestionnaireModal = async () => {
|
||||
await setSurveyList('')
|
||||
setModal(ModalsName.SELECTQUESTIONAIRE, {
|
||||
title: 'Отправить опросник на заполнение',
|
||||
description: 'Все опросники',
|
||||
success: {
|
||||
text: 'Отправить',
|
||||
cb: sendSelectQuestionnaires,
|
||||
},
|
||||
cansel: {
|
||||
text: 'Отмена',
|
||||
cb: closeModal,
|
||||
},
|
||||
data: surveyList.value,
|
||||
})
|
||||
}
|
||||
|
||||
const moveToQuestionnaireStep = () => {
|
||||
patientStep.value = PatientStep.QUESTIONNAIRE
|
||||
}
|
||||
</script>
|
5
src/entities/patient/ui/PatientSurveyCard/index.ts
Normal file
5
src/entities/patient/ui/PatientSurveyCard/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import PatientSurveyCard, {
|
||||
type PatientSurveyCardProps,
|
||||
} from './PatientSurveyCard.vue'
|
||||
|
||||
export { PatientSurveyCard, type PatientSurveyCardProps }
|
27
src/entities/patient/ui/ProgressBar/ProgressBar.scss
Normal file
27
src/entities/patient/ui/ProgressBar/ProgressBar.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.progress {
|
||||
display: flex;
|
||||
|
||||
&.row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: toRem(16);
|
||||
}
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: toRem(8);
|
||||
}
|
||||
&__bar {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--grey-main);
|
||||
}
|
||||
|
||||
.value {
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
background: var(--green-main);
|
||||
}
|
||||
}
|
50
src/entities/patient/ui/ProgressBar/ProgressBar.vue
Normal file
50
src/entities/patient/ui/ProgressBar/ProgressBar.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="bem('bar')" v-bind="barStyle">
|
||||
<div class="value" v-bind="barValueStyle"></div>
|
||||
</div>
|
||||
<span>{{ answers }}/{{ total }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import useBem from 'vue3-bem'
|
||||
|
||||
export type ProgressBarProps = {
|
||||
borderRadius?: string | number
|
||||
percent: string | number | null
|
||||
height?: string
|
||||
total: number
|
||||
answers: number
|
||||
row?: boolean
|
||||
column?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ProgressBarProps>(), {
|
||||
height: '6',
|
||||
borderRadius: '25',
|
||||
row: false,
|
||||
column: false,
|
||||
})
|
||||
|
||||
/** --------------- State ---------------- */
|
||||
const bem = useBem('progress')
|
||||
const classes = computed(() => [
|
||||
...bem(),
|
||||
{ row: props.row },
|
||||
{ column: props.column },
|
||||
])
|
||||
|
||||
const barStyle = computed(() => ({
|
||||
style: {
|
||||
height: `${props.height}px`,
|
||||
'border-radius': `${props.borderRadius}px`,
|
||||
},
|
||||
}))
|
||||
|
||||
const barValueStyle = computed(() => ({
|
||||
style: {
|
||||
width: `${props.percent || 0}%`,
|
||||
},
|
||||
}))
|
||||
</script>
|
3
src/entities/patient/ui/ProgressBar/index.ts
Normal file
3
src/entities/patient/ui/ProgressBar/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import ProgressBar, { type ProgressBarProps } from './ProgressBar.vue'
|
||||
|
||||
export { ProgressBar, type ProgressBarProps }
|
@ -0,0 +1,27 @@
|
||||
.questionnaire-card {
|
||||
&__title {
|
||||
@include fontSize(
|
||||
b-16,
|
||||
(
|
||||
weight: 500,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
display: block;
|
||||
margin-top: toRem(8);
|
||||
@include fontSize(s-13);
|
||||
color: var(--dark-64);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: toRem(20);
|
||||
}
|
||||
|
||||
.card {
|
||||
gap: toRem(20);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div :class="bem('')">
|
||||
<card-component rounded size="m">
|
||||
<template #header>
|
||||
<header>
|
||||
<h4 :class="bem('title')">{{ title }}</h4>
|
||||
<span v-if="!assigned" :class="bem('subtitle')">{{
|
||||
questions
|
||||
}}</span>
|
||||
</header>
|
||||
</template>
|
||||
<div v-if="assigned" :class="bem('body')">
|
||||
<progress-bar
|
||||
:answers="answers"
|
||||
:percent="percent"
|
||||
:total="Number(questions)"
|
||||
height="12"
|
||||
column
|
||||
/>
|
||||
</div>
|
||||
<div :class="bem('actions')">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</card-component>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" bem-block="QuestionnaireCard">
|
||||
import { ProgressBar } from '@/entities'
|
||||
export type QuestionnaireCardProps = {
|
||||
title: string
|
||||
answers?: number
|
||||
questions: number | string
|
||||
percent?: number
|
||||
assigned?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<QuestionnaireCardProps>(), {
|
||||
assigned: false,
|
||||
answers: 0,
|
||||
percent: 0,
|
||||
})
|
||||
</script>
|
5
src/entities/patient/ui/QuestionnaireCard/index.ts
Normal file
5
src/entities/patient/ui/QuestionnaireCard/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import QuestionnaireCard, {
|
||||
type QuestionnaireCardProps,
|
||||
} from './QuestionnaireCard.vue'
|
||||
|
||||
export { QuestionnaireCard, type QuestionnaireCardProps }
|
15
src/entities/patient/ui/index.ts
Normal file
15
src/entities/patient/ui/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export * from './PatientRequest'
|
||||
export * from './PatientNavigation'
|
||||
export * from './ProgressBar'
|
||||
export * from './PatientFilesCard'
|
||||
export * from './PatientHealthMatrix'
|
||||
export * from './PatientBasicInfo'
|
||||
export * from './PatientSurveyCard'
|
||||
export * from './PatientReminders'
|
||||
export * from './EditableCard'
|
||||
export * from './InitialAppointment'
|
||||
export * from './QuestionnaireCard'
|
||||
export * from './EmptySurvey'
|
||||
export * from './InitialHealthMatrix'
|
||||
export * from './InitialPurpose'
|
||||
export * from './EditableInput'
|
16
src/entities/user/api/index.ts
Normal file
16
src/entities/user/api/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import { baseApi } from '@/shared'
|
||||
import type { User } from '../lib'
|
||||
|
||||
export const fetchUser = (): Promise<UserAPI.GET.FetchUser.Response> =>
|
||||
baseApi.get('user')
|
||||
|
||||
export namespace UserAPI {
|
||||
export namespace GET {
|
||||
export namespace FetchUser {
|
||||
export type Response = AxiosResponse<{
|
||||
data: User
|
||||
}>
|
||||
}
|
||||
}
|
||||
}
|
4
src/entities/user/index.ts
Normal file
4
src/entities/user/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './model'
|
||||
export * from './lib'
|
||||
export * from './api'
|
||||
export * from './ui'
|
1
src/entities/user/lib/index.ts
Normal file
1
src/entities/user/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './types'
|
24
src/entities/user/lib/types.ts
Normal file
24
src/entities/user/lib/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export type User = {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
birthdate: Maybe<string>
|
||||
city: Maybe<string>
|
||||
marital: Maybe<string>
|
||||
children_count: Maybe<number>
|
||||
profession: Maybe<string>
|
||||
contact: Maybe<string>
|
||||
anamnesis: Maybe<string>
|
||||
asking: Maybe<string>
|
||||
roles: UserRole[]
|
||||
}
|
||||
|
||||
export type UserRole = {
|
||||
id: number
|
||||
name: UserRoles
|
||||
guard_name: number
|
||||
}
|
||||
|
||||
export enum UserRoles {
|
||||
ADMIN = 'admin',
|
||||
}
|
1
src/entities/user/model/index.ts
Normal file
1
src/entities/user/model/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './module'
|
19
src/entities/user/model/module/index.ts
Normal file
19
src/entities/user/model/module/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { Stores } from '@/shared'
|
||||
import { fetchUser } from '../../api'
|
||||
import type { User } from '../../lib'
|
||||
|
||||
export const useUserStore = defineStore(Stores.USER, () => {
|
||||
const currentUser = ref<Maybe<User>>(null)
|
||||
|
||||
const setCurrentUser = async () => {
|
||||
const { data } = await fetchUser()
|
||||
currentUser.value = data.data
|
||||
}
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
}
|
||||
})
|
17
src/entities/user/ui/UserAvatar/UserAvatar.scss
Normal file
17
src/entities/user/ui/UserAvatar/UserAvatar.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.user-avatar {
|
||||
&__img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.noavatar {
|
||||
background: var(--purple-8-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user